mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-06 05:08:11 +08:00
Add locking layer feature
This commit is contained in:
@@ -97,6 +97,14 @@ pub enum NodeGraphMessage {
|
|||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
hidden: bool,
|
hidden: bool,
|
||||||
},
|
},
|
||||||
|
ToggleSelectedLocked,
|
||||||
|
ToggleLocked {
|
||||||
|
node_id: NodeId,
|
||||||
|
},
|
||||||
|
SetLocked {
|
||||||
|
node_id: NodeId,
|
||||||
|
locked: bool,
|
||||||
|
},
|
||||||
SetName {
|
SetName {
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
@@ -478,6 +478,35 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
|||||||
}
|
}
|
||||||
self.update_selection_action_buttons(document_network, selected_nodes, responses);
|
self.update_selection_action_buttons(document_network, selected_nodes, responses);
|
||||||
}
|
}
|
||||||
|
NodeGraphMessage::ToggleSelectedLocked => {
|
||||||
|
if let Some(network) = document_network.nested_network(&self.network) {
|
||||||
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
|
|
||||||
|
let new_locked = !selected_nodes.selected_nodes().any(|id| network.locked.contains(id));
|
||||||
|
for &node_id in selected_nodes.selected_nodes() {
|
||||||
|
responses.add(NodeGraphMessage::SetLocked { node_id, locked: new_locked });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeGraphMessage::ToggleLocked { node_id } => {
|
||||||
|
if let Some(network) = document_network.nested_network(&self.network) {
|
||||||
|
let new_locked = !network.locked.contains(&node_id);
|
||||||
|
responses.add(NodeGraphMessage::SetLocked { node_id, locked: new_locked });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NodeGraphMessage::SetLocked { node_id, locked } => {
|
||||||
|
if let Some(network) = document_network.nested_network_mut(&self.network) {
|
||||||
|
if !locked {
|
||||||
|
network.locked.retain(|&id| node_id != id);
|
||||||
|
} else if !network.imports.contains(&node_id) && !network.original_outputs().iter().any(|output| output.node_id == node_id) {
|
||||||
|
network.locked.push(node_id);
|
||||||
|
}
|
||||||
|
if network.connected_to_output(node_id) {
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.update_selection_action_buttons(document_network, selected_nodes, responses);
|
||||||
|
}
|
||||||
NodeGraphMessage::SetName { node_id, name } => {
|
NodeGraphMessage::SetName { node_id, name } => {
|
||||||
responses.add(DocumentMessage::StartTransaction);
|
responses.add(DocumentMessage::StartTransaction);
|
||||||
responses.add(NodeGraphMessage::SetNameImpl { node_id, name });
|
responses.add(NodeGraphMessage::SetNameImpl { node_id, name });
|
||||||
@@ -554,7 +583,7 @@ impl NodeGraphMessageHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the buttons for disable and preview
|
/// Updates the buttons for disable, locked and preview
|
||||||
fn update_selection_action_buttons(&mut self, document_network: &NodeNetwork, selected_nodes: &SelectedNodes, responses: &mut VecDeque<Message>) {
|
fn update_selection_action_buttons(&mut self, document_network: &NodeNetwork, selected_nodes: &SelectedNodes, responses: &mut VecDeque<Message>) {
|
||||||
if let Some(network) = document_network.nested_network(&self.network) {
|
if let Some(network) = document_network.nested_network(&self.network) {
|
||||||
let mut widgets = Vec::new();
|
let mut widgets = Vec::new();
|
||||||
@@ -566,6 +595,7 @@ impl NodeGraphMessageHandler {
|
|||||||
if selection.next().is_some() {
|
if selection.next().is_some() {
|
||||||
// Check if any of the selected nodes are disabled
|
// Check if any of the selected nodes are disabled
|
||||||
let is_hidden = selected_nodes.selected_nodes().any(|id| network.disabled.contains(id));
|
let is_hidden = selected_nodes.selected_nodes().any(|id| network.disabled.contains(id));
|
||||||
|
let is_locked = selected_nodes.selected_nodes().any(|id| network.locked.contains(id));
|
||||||
|
|
||||||
// Check if multiple nodes are selected
|
// Check if multiple nodes are selected
|
||||||
let multiple_nodes = selection.next().is_some();
|
let multiple_nodes = selection.next().is_some();
|
||||||
@@ -580,6 +610,15 @@ impl NodeGraphMessageHandler {
|
|||||||
.widget_holder();
|
.widget_holder();
|
||||||
widgets.push(hide_button);
|
widgets.push(hide_button);
|
||||||
|
|
||||||
|
let (lock_unlock_label, lock_unlock_icon) = if is_locked { ("Make Unlock", "Lock") } else { ("Make Lock", "Unlock") };
|
||||||
|
let lock_button = TextButton::new(lock_unlock_label)
|
||||||
|
.icon(Some(lock_unlock_icon.to_string()))
|
||||||
|
.tooltip(if is_locked { "Unlock selected nodes/layers" } else { "Lock selected nodes/layers" }.to_string() + if multiple_nodes { "s" } else { "" })
|
||||||
|
.tooltip_shortcut(action_keys!(NodeGraphMessageDiscriminant::ToggleLocked))
|
||||||
|
.on_update(move |_| NodeGraphMessage::ToggleSelectedLocked.into())
|
||||||
|
.widget_holder();
|
||||||
|
widgets.push(lock_button);
|
||||||
|
|
||||||
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,6 +785,7 @@ impl NodeGraphMessageHandler {
|
|||||||
position: node.metadata.position.into(),
|
position: node.metadata.position.into(),
|
||||||
previewed: network.outputs_contain(node_id),
|
previewed: network.outputs_contain(node_id),
|
||||||
disabled: network.disabled.contains(&node_id),
|
disabled: network.disabled.contains(&node_id),
|
||||||
|
locked: network.locked.contains(&node_id),
|
||||||
errors: errors.map(|e| format!("{e:?}")),
|
errors: errors.map(|e| format!("{e:?}")),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -775,6 +815,7 @@ impl NodeGraphMessageHandler {
|
|||||||
name: network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(),
|
name: network.nodes.get(&node_id).map(|node| node.alias.clone()).unwrap_or_default(),
|
||||||
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
|
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
|
||||||
disabled: network.disabled.contains(&node_id),
|
disabled: network.disabled.contains(&node_id),
|
||||||
|
locked: network.locked.contains(&node_id),
|
||||||
};
|
};
|
||||||
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
|
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ pub struct FrontendNode {
|
|||||||
pub exposed_outputs: Vec<FrontendGraphOutput>,
|
pub exposed_outputs: Vec<FrontendGraphOutput>,
|
||||||
pub position: (i32, i32),
|
pub position: (i32, i32),
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
|
pub locked: bool,
|
||||||
pub previewed: bool,
|
pub previewed: bool,
|
||||||
pub errors: Option<String>,
|
pub errors: Option<String>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ pub struct LayerPanelEntry {
|
|||||||
pub layer_classification: LayerClassification,
|
pub layer_classification: LayerClassification,
|
||||||
pub expanded: bool,
|
pub expanded: bool,
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
|
pub locked: bool,
|
||||||
#[serde(rename = "parentId")]
|
#[serde(rename = "parentId")]
|
||||||
pub parent_id: Option<NodeId>,
|
pub parent_id: Option<NodeId>,
|
||||||
pub depth: usize,
|
pub depth: usize,
|
||||||
@@ -64,6 +65,14 @@ impl SelectedNodes {
|
|||||||
self.selected_layers(metadata).filter(move |&layer| self.layer_visible(layer, network, metadata))
|
self.selected_layers(metadata).filter(move |&layer| self.layer_visible(layer, network, metadata))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn layer_ulocked(&self, layer: LayerNodeIdentifier, network: &NodeNetwork, metadata: &DocumentMetadata) -> bool {
|
||||||
|
!layer.ancestors(metadata).any(|layer| network.locked.contains(&layer.to_node()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selected_ulocked_layers<'a>(&'a self, network: &'a NodeNetwork, metadata: &'a DocumentMetadata) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||||
|
self.selected_layers(metadata).filter(move |&layer| self.layer_ulocked(layer, network, metadata))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selected_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
pub fn selected_layers<'a>(&'a self, metadata: &'a DocumentMetadata) -> impl Iterator<Item = LayerNodeIdentifier> + '_ {
|
||||||
metadata.all_layers().filter(|layer| self.0.contains(&layer.to_node()))
|
metadata.all_layers().filter(|layer| self.0.contains(&layer.to_node()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
|
<path d="M8,3C3,3,0,8,0,8s3,5,8,5s8-5,8-5S13,3,8,3z M8,12c-2.2,0-4-1.8-4-4s1.8-4,4-4s4,1.8,4,4S10.2,12,8,12z" />
|
||||||
|
<circle cx="9" cy="7" r="1.2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 216 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||||
|
<path d="M8,4c3.5,0,5.9,2.8,6.8,4c-0.9,1.2-3.3,4-6.8,4S2.1,9.2,1.2,8C2.1,6.8,4.5,4,8,4 M8,3C3,3,0,8,0,8s3,5,8,5s8-5,8-5S13,3,8,3L8,3z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 207 B |
@@ -133,6 +133,10 @@
|
|||||||
editor.instance.toggleLayerVisibility(id);
|
editor.instance.toggleLayerVisibility(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleLayerLock(id: bigint) {
|
||||||
|
editor.instance.toggleLayerLock(id);
|
||||||
|
}
|
||||||
|
|
||||||
function handleExpandArrowClick(id: bigint) {
|
function handleExpandArrowClick(id: bigint) {
|
||||||
editor.instance.toggleLayerExpansion(id);
|
editor.instance.toggleLayerExpansion(id);
|
||||||
}
|
}
|
||||||
@@ -420,6 +424,13 @@
|
|||||||
icon={listing.entry.disabled ? "EyeHidden" : "EyeVisible"}
|
icon={listing.entry.disabled ? "EyeHidden" : "EyeVisible"}
|
||||||
tooltip={listing.entry.disabled ? "Disabled" : "Enabled"}
|
tooltip={listing.entry.disabled ? "Disabled" : "Enabled"}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
class={"visibility"}
|
||||||
|
action={(e) => (toggleLayerLock(listing.entry.id), e?.stopPropagation())}
|
||||||
|
size={24}
|
||||||
|
icon={listing.entry.locked ? "Lock" : "Unlock"}
|
||||||
|
tooltip={listing.entry.locked ? "Locked" : "Unlocked"}
|
||||||
|
/>
|
||||||
</LayoutRow>
|
</LayoutRow>
|
||||||
{/each}
|
{/each}
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ import IconsGrid from "@graphite-frontend/assets/icon-16px-solid/icons-grid.svg"
|
|||||||
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
|
import Image from "@graphite-frontend/assets/icon-16px-solid/image.svg";
|
||||||
import Layer from "@graphite-frontend/assets/icon-16px-solid/layer.svg";
|
import Layer from "@graphite-frontend/assets/icon-16px-solid/layer.svg";
|
||||||
import License from "@graphite-frontend/assets/icon-16px-solid/license.svg";
|
import License from "@graphite-frontend/assets/icon-16px-solid/license.svg";
|
||||||
|
import Lock from "@graphite-frontend/assets/icon-16px-solid/lock.svg";
|
||||||
import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.svg";
|
import NodeBlur from "@graphite-frontend/assets/icon-16px-solid/node-blur.svg";
|
||||||
import NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.svg";
|
import NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.svg";
|
||||||
import NodeColorCorrection from "@graphite-frontend/assets/icon-16px-solid/node-color-correction.svg";
|
import NodeColorCorrection from "@graphite-frontend/assets/icon-16px-solid/node-color-correction.svg";
|
||||||
@@ -140,6 +141,7 @@ import Rescale from "@graphite-frontend/assets/icon-16px-solid/rescale.svg";
|
|||||||
import Reset from "@graphite-frontend/assets/icon-16px-solid/reset.svg";
|
import Reset from "@graphite-frontend/assets/icon-16px-solid/reset.svg";
|
||||||
import Settings from "@graphite-frontend/assets/icon-16px-solid/settings.svg";
|
import Settings from "@graphite-frontend/assets/icon-16px-solid/settings.svg";
|
||||||
import Trash from "@graphite-frontend/assets/icon-16px-solid/trash.svg";
|
import Trash from "@graphite-frontend/assets/icon-16px-solid/trash.svg";
|
||||||
|
import Unlock from "@graphite-frontend/assets/icon-16px-solid/unlock.svg";
|
||||||
import ViewModeNormal from "@graphite-frontend/assets/icon-16px-solid/view-mode-normal.svg";
|
import ViewModeNormal from "@graphite-frontend/assets/icon-16px-solid/view-mode-normal.svg";
|
||||||
import ViewModeOutline from "@graphite-frontend/assets/icon-16px-solid/view-mode-outline.svg";
|
import ViewModeOutline from "@graphite-frontend/assets/icon-16px-solid/view-mode-outline.svg";
|
||||||
import ViewModePixels from "@graphite-frontend/assets/icon-16px-solid/view-mode-pixels.svg";
|
import ViewModePixels from "@graphite-frontend/assets/icon-16px-solid/view-mode-pixels.svg";
|
||||||
@@ -187,6 +189,7 @@ const SOLID_16PX = {
|
|||||||
Image: { svg: Image, size: 16 },
|
Image: { svg: Image, size: 16 },
|
||||||
Layer: { svg: Layer, size: 16 },
|
Layer: { svg: Layer, size: 16 },
|
||||||
License: { svg: License, size: 16 },
|
License: { svg: License, size: 16 },
|
||||||
|
Lock: { svg: Lock, size: 16 },
|
||||||
NodeBlur: { svg: NodeBlur, size: 16 },
|
NodeBlur: { svg: NodeBlur, size: 16 },
|
||||||
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
|
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
|
||||||
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
|
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
|
||||||
@@ -208,6 +211,7 @@ const SOLID_16PX = {
|
|||||||
Reset: { svg: Reset, size: 16 },
|
Reset: { svg: Reset, size: 16 },
|
||||||
Settings: { svg: Settings, size: 16 },
|
Settings: { svg: Settings, size: 16 },
|
||||||
Trash: { svg: Trash, size: 16 },
|
Trash: { svg: Trash, size: 16 },
|
||||||
|
Unlock: { svg: Unlock, size: 16 },
|
||||||
ViewModeNormal: { svg: ViewModeNormal, size: 16 },
|
ViewModeNormal: { svg: ViewModeNormal, size: 16 },
|
||||||
ViewModeOutline: { svg: ViewModeOutline, size: 16 },
|
ViewModeOutline: { svg: ViewModeOutline, size: 16 },
|
||||||
ViewModePixels: { svg: ViewModePixels, size: 16 },
|
ViewModePixels: { svg: ViewModePixels, size: 16 },
|
||||||
|
|||||||
@@ -126,6 +126,8 @@ export class FrontendNode {
|
|||||||
|
|
||||||
readonly disabled!: boolean;
|
readonly disabled!: boolean;
|
||||||
|
|
||||||
|
readonly locked!: boolean;
|
||||||
|
|
||||||
readonly errors!: string | undefined;
|
readonly errors!: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,6 +617,8 @@ export class LayerPanelEntry {
|
|||||||
|
|
||||||
disabled!: boolean;
|
disabled!: boolean;
|
||||||
|
|
||||||
|
locked!: boolean;
|
||||||
|
|
||||||
parentId!: bigint | undefined;
|
parentId!: bigint | undefined;
|
||||||
|
|
||||||
id!: bigint;
|
id!: bigint;
|
||||||
|
|||||||
@@ -762,6 +762,14 @@ impl JsEditorHandle {
|
|||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle lock state of a layer from the layer list
|
||||||
|
#[wasm_bindgen(js_name = toggleLayerLock)]
|
||||||
|
pub fn toggle_layer_lock(&self, id: u64) {
|
||||||
|
let id = NodeId(id);
|
||||||
|
let message = NodeGraphMessage::ToggleLocked { node_id: id };
|
||||||
|
self.dispatch(message);
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle expansions state of a layer from the layer list
|
/// Toggle expansions state of a layer from the layer list
|
||||||
#[wasm_bindgen(js_name = toggleLayerExpansion)]
|
#[wasm_bindgen(js_name = toggleLayerExpansion)]
|
||||||
pub fn toggle_layer_expansion(&self, id: u64) {
|
pub fn toggle_layer_expansion(&self, id: u64) {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ fn add_network() -> NodeNetwork {
|
|||||||
imports: vec![],
|
imports: vec![],
|
||||||
exports: vec![NodeOutput::new(NodeId(0), 0)],
|
exports: vec![NodeOutput::new(NodeId(0), 0)],
|
||||||
disabled: vec![],
|
disabled: vec![],
|
||||||
|
locked: vec![],
|
||||||
previous_outputs: None,
|
previous_outputs: None,
|
||||||
nodes: [DocumentNode {
|
nodes: [DocumentNode {
|
||||||
name: "Blend Image".into(),
|
name: "Blend Image".into(),
|
||||||
|
|||||||
@@ -526,6 +526,7 @@ pub struct NodeNetwork {
|
|||||||
/// Nodes that the user has disabled/hidden with the visibility eye icon.
|
/// Nodes that the user has disabled/hidden with the visibility eye icon.
|
||||||
/// These nodes get replaced with Identity nodes during the graph flattening step.
|
/// These nodes get replaced with Identity nodes during the graph flattening step.
|
||||||
pub disabled: Vec<NodeId>,
|
pub disabled: Vec<NodeId>,
|
||||||
|
pub locked: Vec<NodeId>,
|
||||||
/// In the case when another node is previewed (chosen by the user as a temporary output), this stores what it previously was so it can be restored later.
|
/// In the case when another node is previewed (chosen by the user as a temporary output), this stores what it previously was so it can be restored later.
|
||||||
pub previous_outputs: Option<Vec<NodeOutput>>,
|
pub previous_outputs: Option<Vec<NodeOutput>>,
|
||||||
}
|
}
|
||||||
@@ -541,6 +542,7 @@ impl std::hash::Hash for NodeNetwork {
|
|||||||
node.hash(state);
|
node.hash(state);
|
||||||
}
|
}
|
||||||
self.disabled.hash(state);
|
self.disabled.hash(state);
|
||||||
|
self.locked.hash(state);
|
||||||
self.previous_outputs.hash(state);
|
self.previous_outputs.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,6 +570,7 @@ impl NodeNetwork {
|
|||||||
exports: vec![NodeOutput::new(NodeId(0), 0)],
|
exports: vec![NodeOutput::new(NodeId(0), 0)],
|
||||||
nodes: [(NodeId(0), node)].into_iter().collect(),
|
nodes: [(NodeId(0), node)].into_iter().collect(),
|
||||||
disabled: vec![],
|
disabled: vec![],
|
||||||
|
locked: vec![],
|
||||||
previous_outputs: None,
|
previous_outputs: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -816,6 +819,7 @@ impl NodeNetwork {
|
|||||||
self.imports.iter_mut().for_each(|id| *id = f(*id));
|
self.imports.iter_mut().for_each(|id| *id = f(*id));
|
||||||
self.exports.iter_mut().for_each(|output| output.node_id = f(output.node_id));
|
self.exports.iter_mut().for_each(|output| output.node_id = f(output.node_id));
|
||||||
self.disabled.iter_mut().for_each(|id| *id = f(*id));
|
self.disabled.iter_mut().for_each(|id| *id = f(*id));
|
||||||
|
self.locked.iter_mut().for_each(|id| *id = f(*id));
|
||||||
self.previous_outputs
|
self.previous_outputs
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.for_each(|nodes| nodes.iter_mut().for_each(|output| output.node_id = f(output.node_id)));
|
.for_each(|nodes| nodes.iter_mut().for_each(|output| output.node_id = f(output.node_id)));
|
||||||
@@ -984,6 +988,7 @@ impl NodeNetwork {
|
|||||||
// Copy nodes from the inner network into the parent network
|
// Copy nodes from the inner network into the parent network
|
||||||
self.nodes.extend(inner_network.nodes);
|
self.nodes.extend(inner_network.nodes);
|
||||||
self.disabled.extend(inner_network.disabled);
|
self.disabled.extend(inner_network.disabled);
|
||||||
|
self.locked.extend(inner_network.locked);
|
||||||
|
|
||||||
let mut network_offsets = HashMap::new();
|
let mut network_offsets = HashMap::new();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
Reference in New Issue
Block a user