Group layers with Ctrl+G into independent groups if they're spread across artboards (#2239)
* Continuation of first attempt with unmerged PR #1992 * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
5a8eb9dd1b
commit
86f09be0ee
|
|
@ -7,6 +7,7 @@ use crate::messages::input_mapper::utility_types::misc::MappingEntry;
|
|||
use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping};
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::Direction;
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate;
|
||||
use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys;
|
||||
|
|
@ -333,7 +334,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument),
|
||||
entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers),
|
||||
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers),
|
||||
entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }),
|
||||
entry!(KeyDown(KeyG); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::UngroupSelectedLayers),
|
||||
entry!(KeyDown(KeyN); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::CreateEmptyFolder),
|
||||
entry!(KeyDown(BracketLeft); modifiers=[Alt], action_dispatch=DocumentMessage::SelectionStepBack),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use super::utility_types::misc::SnappingState;
|
||||
use super::utility_types::misc::{GroupFolderType, SnappingState};
|
||||
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
|
|
@ -39,9 +39,6 @@ pub enum DocumentMessage {
|
|||
},
|
||||
ClearArtboards,
|
||||
ClearLayersPanel,
|
||||
InsertBooleanOperation {
|
||||
operation: graphene_core::vector::misc::BooleanOperation,
|
||||
},
|
||||
CreateEmptyFolder,
|
||||
DebugPrintDocument,
|
||||
DeleteNode {
|
||||
|
|
@ -71,7 +68,9 @@ pub enum DocumentMessage {
|
|||
GridOptions(GridSnapping),
|
||||
GridOverlays(OverlayContext),
|
||||
GridVisibility(bool),
|
||||
GroupSelectedLayers,
|
||||
GroupSelectedLayers {
|
||||
group_folder_type: GroupFolderType,
|
||||
},
|
||||
ImaginateGenerate {
|
||||
imaginate_node: Vec<NodeId>,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use super::node_graph::utility_types::Transform;
|
|||
use super::overlays::utility_types::Pivot;
|
||||
use super::utility_types::clipboards::Clipboard;
|
||||
use super::utility_types::error::EditorError;
|
||||
use super::utility_types::misc::{SnappingOptions, SnappingState, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS};
|
||||
use super::utility_types::misc::{GroupFolderType, SnappingOptions, SnappingState, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS};
|
||||
use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus};
|
||||
use super::utility_types::nodes::{CollapsedLayers, SelectedNodes};
|
||||
use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH};
|
||||
|
|
@ -285,41 +285,16 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
layout_target: LayoutTarget::LayersPanelControlBar,
|
||||
});
|
||||
}
|
||||
DocumentMessage::InsertBooleanOperation { operation } => {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
||||
let Some(parent) = self.network_interface.deepest_common_ancestor(&[], false) else {
|
||||
// Cancel grouping layers across different artboards
|
||||
// TODO: Group each set of layers for each artboard separately
|
||||
return;
|
||||
};
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent);
|
||||
|
||||
let folder_id = NodeId::new();
|
||||
let boolean_operation_layer = LayerNodeIdentifier::new_unchecked(folder_id);
|
||||
responses.add(GraphOperationMessage::NewBooleanOperationLayer {
|
||||
id: folder_id,
|
||||
operation,
|
||||
parent,
|
||||
insert_index,
|
||||
});
|
||||
responses.add(NodeGraphMessage::SetDisplayNameImpl {
|
||||
node_id: folder_id,
|
||||
alias: "Boolean Operation".to_string(),
|
||||
});
|
||||
// Move all shallowest selected layers as children
|
||||
responses.add(DocumentMessage::MoveSelectedLayersToGroup { parent: boolean_operation_layer });
|
||||
}
|
||||
DocumentMessage::CreateEmptyFolder => {
|
||||
let selected_nodes = self.network_interface.selected_nodes(&[]).unwrap();
|
||||
let id = NodeId::new();
|
||||
|
||||
let parent = self
|
||||
.network_interface
|
||||
.deepest_common_ancestor(&self.selection_network_path, true)
|
||||
.deepest_common_ancestor(&selected_nodes, &self.selection_network_path, true)
|
||||
.unwrap_or(LayerNodeIdentifier::ROOT_PARENT);
|
||||
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent);
|
||||
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &self.network_interface.selected_nodes(&[]).unwrap(), parent);
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
responses.add(GraphOperationMessage::NewCustomLayer {
|
||||
id,
|
||||
|
|
@ -380,7 +355,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
DocumentMessage::DuplicateSelectedLayers => {
|
||||
let parent = self.new_layer_parent(false);
|
||||
let calculated_insert_index =
|
||||
DocumentMessageHandler::get_calculated_insert_index(self.network_interface.document_metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent);
|
||||
DocumentMessageHandler::get_calculated_insert_index(self.network_interface.document_metadata(), &self.network_interface.selected_nodes(&[]).unwrap(), parent);
|
||||
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
responses.add(PortfolioMessage::Copy { clipboard: Clipboard::Internal });
|
||||
|
|
@ -491,33 +466,53 @@ impl MessageHandler<DocumentMessage, DocumentMessageData<'_>> for DocumentMessag
|
|||
self.snapping_state.grid_snapping = enabled;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
}
|
||||
DocumentMessage::GroupSelectedLayers => {
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type } => {
|
||||
responses.add(DocumentMessage::AddTransaction);
|
||||
|
||||
let Some(parent) = self.network_interface.deepest_common_ancestor(&self.selection_network_path, false) else {
|
||||
// Cancel grouping layers across different artboards
|
||||
// TODO: Group each set of layers for each artboard separately
|
||||
return;
|
||||
};
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent);
|
||||
let mut parent_per_selected_nodes: HashMap<LayerNodeIdentifier, Vec<NodeId>> = HashMap::new();
|
||||
let artboards = LayerNodeIdentifier::ROOT_PARENT
|
||||
.children(self.metadata())
|
||||
.filter(|x| self.network_interface.is_artboard(&x.to_node(), &self.selection_network_path))
|
||||
.collect::<Vec<_>>();
|
||||
let Some(selected_nodes) = self.network_interface.selected_nodes(&[]) else { return };
|
||||
|
||||
let node_id = NodeId::new();
|
||||
let new_group_node = document_node_definitions::resolve_document_node_type("Merge")
|
||||
.expect("Failed to create merge node")
|
||||
.default_node_template();
|
||||
responses.add(NodeGraphMessage::InsertNode {
|
||||
node_id,
|
||||
node_template: new_group_node,
|
||||
});
|
||||
let new_group_folder = LayerNodeIdentifier::new_unchecked(node_id);
|
||||
// Move the new folder to the correct position
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack {
|
||||
layer: new_group_folder,
|
||||
parent,
|
||||
insert_index,
|
||||
});
|
||||
// Non-artboard (infinite canvas) workflow
|
||||
if artboards.is_empty() {
|
||||
let Some(parent) = self.network_interface.deepest_common_ancestor(&selected_nodes, &self.selection_network_path, false) else {
|
||||
return;
|
||||
};
|
||||
let Some(selected_nodes) = &self.network_interface.selected_nodes(&self.selection_network_path) else {
|
||||
return;
|
||||
};
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), selected_nodes, parent);
|
||||
|
||||
responses.add(DocumentMessage::MoveSelectedLayersToGroup { parent: new_group_folder });
|
||||
DocumentMessageHandler::group_layers(responses, insert_index, parent, group_folder_type);
|
||||
}
|
||||
// Artboard workflow
|
||||
else {
|
||||
for artboard in artboards {
|
||||
let selected_descendants = artboard.descendants(self.metadata()).filter(|x| selected_nodes.selected_layers_contains(*x, self.metadata()));
|
||||
for selected_descendant in selected_descendants {
|
||||
parent_per_selected_nodes.entry(artboard).or_default().push(selected_descendant.to_node());
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_folders: Vec<NodeId> = Vec::new();
|
||||
|
||||
for children in parent_per_selected_nodes.into_values() {
|
||||
let child_selected_nodes = SelectedNodes(children);
|
||||
let Some(parent) = self.network_interface.deepest_common_ancestor(&child_selected_nodes, &self.selection_network_path, false) else {
|
||||
continue;
|
||||
};
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &child_selected_nodes, parent);
|
||||
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: child_selected_nodes.0 });
|
||||
|
||||
new_folders.push(DocumentMessageHandler::group_layers(responses, insert_index, parent, group_folder_type));
|
||||
}
|
||||
|
||||
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_folders });
|
||||
}
|
||||
}
|
||||
DocumentMessage::ImaginateGenerate { imaginate_node } => {
|
||||
let random_value = generate_uuid();
|
||||
|
|
@ -1710,12 +1705,17 @@ impl DocumentMessageHandler {
|
|||
|
||||
/// Finds the parent folder which, based on the current selections, should be the container of any newly added layers.
|
||||
pub fn new_layer_parent(&self, include_self: bool) -> LayerNodeIdentifier {
|
||||
let Some(selected_nodes) = self.network_interface.selected_nodes(&self.selection_network_path) else {
|
||||
warn!("No selected nodes found in new_layer_parent. Defaulting to ROOT_PARENT.");
|
||||
return LayerNodeIdentifier::ROOT_PARENT;
|
||||
};
|
||||
|
||||
self.network_interface
|
||||
.deepest_common_ancestor(&self.selection_network_path, include_self)
|
||||
.deepest_common_ancestor(&selected_nodes, &self.selection_network_path, include_self)
|
||||
.unwrap_or_else(|| self.network_interface.all_artboards().iter().next().copied().unwrap_or(LayerNodeIdentifier::ROOT_PARENT))
|
||||
}
|
||||
|
||||
pub fn get_calculated_insert_index(metadata: &DocumentMetadata, selected_nodes: SelectedNodes, parent: LayerNodeIdentifier) -> usize {
|
||||
pub fn get_calculated_insert_index(metadata: &DocumentMetadata, selected_nodes: &SelectedNodes, parent: LayerNodeIdentifier) -> usize {
|
||||
parent
|
||||
.children(metadata)
|
||||
.enumerate()
|
||||
|
|
@ -1735,6 +1735,36 @@ impl DocumentMessageHandler {
|
|||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn group_layers(responses: &mut VecDeque<Message>, insert_index: usize, parent: LayerNodeIdentifier, group_folder_type: GroupFolderType) -> NodeId {
|
||||
let folder_id = NodeId(generate_uuid());
|
||||
match group_folder_type {
|
||||
GroupFolderType::Layer => responses.add(GraphOperationMessage::NewCustomLayer {
|
||||
id: folder_id,
|
||||
nodes: Vec::new(),
|
||||
parent,
|
||||
insert_index,
|
||||
}),
|
||||
GroupFolderType::BooleanOperation(operation) => {
|
||||
responses.add(GraphOperationMessage::NewBooleanOperationLayer {
|
||||
id: folder_id,
|
||||
operation,
|
||||
parent,
|
||||
insert_index,
|
||||
});
|
||||
}
|
||||
};
|
||||
let new_group_folder = LayerNodeIdentifier::new_unchecked(folder_id);
|
||||
// Move the new folder to the correct position
|
||||
responses.add(NodeGraphMessage::MoveLayerToStack {
|
||||
layer: new_group_folder,
|
||||
parent,
|
||||
insert_index,
|
||||
});
|
||||
responses.add(DocumentMessage::MoveSelectedLayersToGroup { parent: new_group_folder });
|
||||
|
||||
folder_id
|
||||
}
|
||||
|
||||
/// Loads all of the fonts in the document.
|
||||
pub fn load_layer_resources(&self, responses: &mut VecDeque<Message>) {
|
||||
let mut fonts = HashSet::new();
|
||||
|
|
@ -2055,7 +2085,10 @@ impl DocumentMessageHandler {
|
|||
IconButton::new("Folder", 24)
|
||||
.tooltip("Group Selected")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
|
||||
.on_update(|_| DocumentMessage::GroupSelectedLayers.into())
|
||||
.on_update(|_| {
|
||||
let group_folder_type = GroupFolderType::Layer;
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||
})
|
||||
.disabled(!has_selection)
|
||||
.widget_holder(),
|
||||
IconButton::new("Trash", 24)
|
||||
|
|
|
|||
|
|
@ -149,6 +149,10 @@ impl MessageHandler<GraphOperationMessage, GraphOperationMessageData<'_>> for Gr
|
|||
let layer = modify_inputs.create_layer(id);
|
||||
modify_inputs.insert_boolean_data(operation, layer);
|
||||
network_interface.move_layer_to_stack(layer, parent, insert_index, &[]);
|
||||
responses.add(NodeGraphMessage::SetDisplayNameImpl {
|
||||
node_id: id,
|
||||
alias: "Boolean Operation".to_string(),
|
||||
});
|
||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||
}
|
||||
GraphOperationMessage::NewCustomLayer { id, nodes, parent, insert_index } => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Modify
|
|||
use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext;
|
||||
use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType};
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{
|
||||
self, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource,
|
||||
};
|
||||
|
|
@ -1815,7 +1816,10 @@ impl NodeGraphMessageHandler {
|
|||
IconButton::new("Folder", 24)
|
||||
.tooltip("Group Selected")
|
||||
.tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers))
|
||||
.on_update(|_| DocumentMessage::GroupSelectedLayers.into())
|
||||
.on_update(|_| {
|
||||
let group_folder_type = GroupFolderType::Layer;
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||
})
|
||||
.disabled(!has_selection)
|
||||
.widget_holder(),
|
||||
IconButton::new("Trash", 24)
|
||||
|
|
|
|||
|
|
@ -668,3 +668,9 @@ impl PTZ {
|
|||
self.zoom = zoom.clamp(crate::consts::VIEWPORT_ZOOM_SCALE_MIN, crate::consts::VIEWPORT_ZOOM_SCALE_MAX)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum GroupFolderType {
|
||||
Layer,
|
||||
BooleanOperation(graphene_std::vector::misc::BooleanOperation),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1365,15 +1365,11 @@ impl NodeNetworkInterface {
|
|||
}
|
||||
|
||||
/// Ancestor that is shared by all layers and that is deepest (more nested). Default may be the root. Skips selected non-folder, non-artboard layers
|
||||
pub fn deepest_common_ancestor(&self, network_path: &[NodeId], include_self: bool) -> Option<LayerNodeIdentifier> {
|
||||
pub fn deepest_common_ancestor(&self, selected_nodes: &SelectedNodes, network_path: &[NodeId], include_self: bool) -> Option<LayerNodeIdentifier> {
|
||||
if !network_path.is_empty() {
|
||||
log::error!("Currently can only get deepest common ancestor in the document network");
|
||||
return None;
|
||||
}
|
||||
let Some(selected_nodes) = self.selected_nodes(network_path) else {
|
||||
log::error!("Could not get selected nodes in deepest_common_ancestor");
|
||||
return None;
|
||||
};
|
||||
selected_nodes
|
||||
.selected_layers(&self.document_metadata)
|
||||
.map(|layer| {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::messages::input_mapper::utility_types::macros::action_keys;
|
||||
use crate::messages::layout::utility_types::widget_prelude::*;
|
||||
use crate::messages::portfolio::document::utility_types::clipboards::Clipboard;
|
||||
use crate::messages::portfolio::document::utility_types::misc::GroupFolderType;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
pub struct MenuBarMessageData {
|
||||
|
|
@ -226,7 +227,12 @@ impl LayoutHolder for MenuBarMessageHandler {
|
|||
label: "Group Selected".into(),
|
||||
icon: Some("Folder".into()),
|
||||
shortcut: action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers),
|
||||
action: MenuBarEntry::create_action(|_| DocumentMessage::GroupSelectedLayers.into()),
|
||||
action: MenuBarEntry::create_action(|_| {
|
||||
DocumentMessage::GroupSelectedLayers {
|
||||
group_folder_type: GroupFolderType::Layer,
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
disabled: no_active_document || !has_selected_layers,
|
||||
..MenuBarEntry::default()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
|||
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis};
|
||||
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType};
|
||||
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate};
|
||||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::preferences::SelectionMode;
|
||||
|
|
@ -158,7 +158,10 @@ impl SelectTool {
|
|||
IconButton::new(icon, 24)
|
||||
.tooltip(operation.to_string())
|
||||
.disabled(selected_count == 0)
|
||||
.on_update(move |_| DocumentMessage::InsertBooleanOperation { operation }.into())
|
||||
.on_update(move |_| {
|
||||
let group_folder_type = GroupFolderType::BooleanOperation(operation);
|
||||
DocumentMessage::GroupSelectedLayers { group_folder_type }.into()
|
||||
})
|
||||
.widget_holder()
|
||||
})
|
||||
}
|
||||
|
|
@ -354,7 +357,7 @@ impl SelectToolData {
|
|||
|
||||
let nodes = document.network_interface.copy_nodes(©_ids, &[]).collect::<Vec<(NodeId, NodeTemplate)>>();
|
||||
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(document.metadata(), document.network_interface.selected_nodes(&[]).unwrap(), parent);
|
||||
let insert_index = DocumentMessageHandler::get_calculated_insert_index(document.metadata(), &document.network_interface.selected_nodes(&[]).unwrap(), parent);
|
||||
|
||||
let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue