mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-19 02:48:12 +08:00
Add locking layer feature
This commit is contained in:
@@ -97,6 +97,14 @@ pub enum NodeGraphMessage {
|
||||
node_id: NodeId,
|
||||
hidden: bool,
|
||||
},
|
||||
ToggleSelectedLocked,
|
||||
ToggleLocked {
|
||||
node_id: NodeId,
|
||||
},
|
||||
SetLocked {
|
||||
node_id: NodeId,
|
||||
locked: bool,
|
||||
},
|
||||
SetName {
|
||||
node_id: NodeId,
|
||||
name: String,
|
||||
|
||||
@@ -478,6 +478,35 @@ impl<'a> MessageHandler<NodeGraphMessage, NodeGraphHandlerData<'a>> for NodeGrap
|
||||
}
|
||||
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 } => {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
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>) {
|
||||
if let Some(network) = document_network.nested_network(&self.network) {
|
||||
let mut widgets = Vec::new();
|
||||
@@ -566,6 +595,7 @@ impl NodeGraphMessageHandler {
|
||||
if selection.next().is_some() {
|
||||
// Check if any of the selected nodes are disabled
|
||||
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
|
||||
let multiple_nodes = selection.next().is_some();
|
||||
@@ -580,6 +610,15 @@ impl NodeGraphMessageHandler {
|
||||
.widget_holder();
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -746,6 +785,7 @@ impl NodeGraphMessageHandler {
|
||||
position: node.metadata.position.into(),
|
||||
previewed: network.outputs_contain(node_id),
|
||||
disabled: network.disabled.contains(&node_id),
|
||||
locked: network.locked.contains(&node_id),
|
||||
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(),
|
||||
tooltip: if cfg!(debug_assertions) { format!("Layer ID: {node_id}") } else { "".into() },
|
||||
disabled: network.disabled.contains(&node_id),
|
||||
locked: network.locked.contains(&node_id),
|
||||
};
|
||||
responses.add(FrontendMessage::UpdateDocumentLayerDetails { data });
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ pub struct FrontendNode {
|
||||
pub exposed_outputs: Vec<FrontendGraphOutput>,
|
||||
pub position: (i32, i32),
|
||||
pub disabled: bool,
|
||||
pub locked: bool,
|
||||
pub previewed: bool,
|
||||
pub errors: Option<String>,
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ pub struct LayerPanelEntry {
|
||||
pub layer_classification: LayerClassification,
|
||||
pub expanded: bool,
|
||||
pub disabled: bool,
|
||||
pub locked: bool,
|
||||
#[serde(rename = "parentId")]
|
||||
pub parent_id: Option<NodeId>,
|
||||
pub depth: usize,
|
||||
@@ -64,6 +65,14 @@ impl SelectedNodes {
|
||||
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> + '_ {
|
||||
metadata.all_layers().filter(|layer| self.0.contains(&layer.to_node()))
|
||||
}
|
||||
|
||||
4
frontend/assets/icon-16px-solid/lock.svg
Normal file
4
frontend/assets/icon-16px-solid/lock.svg
Normal file
@@ -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 |
3
frontend/assets/icon-16px-solid/unlock.svg
Normal file
3
frontend/assets/icon-16px-solid/unlock.svg
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
function toggleLayerLock(id: bigint) {
|
||||
editor.instance.toggleLayerLock(id);
|
||||
}
|
||||
|
||||
function handleExpandArrowClick(id: bigint) {
|
||||
editor.instance.toggleLayerExpansion(id);
|
||||
}
|
||||
@@ -420,6 +424,13 @@
|
||||
icon={listing.entry.disabled ? "EyeHidden" : "EyeVisible"}
|
||||
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>
|
||||
{/each}
|
||||
</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 Layer from "@graphite-frontend/assets/icon-16px-solid/layer.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 NodeBrushwork from "@graphite-frontend/assets/icon-16px-solid/node-brushwork.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 Settings from "@graphite-frontend/assets/icon-16px-solid/settings.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 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";
|
||||
@@ -187,6 +189,7 @@ const SOLID_16PX = {
|
||||
Image: { svg: Image, size: 16 },
|
||||
Layer: { svg: Layer, size: 16 },
|
||||
License: { svg: License, size: 16 },
|
||||
Lock: { svg: Lock, size: 16 },
|
||||
NodeBlur: { svg: NodeBlur, size: 16 },
|
||||
NodeBrushwork: { svg: NodeBrushwork, size: 16 },
|
||||
NodeColorCorrection: { svg: NodeColorCorrection, size: 16 },
|
||||
@@ -208,6 +211,7 @@ const SOLID_16PX = {
|
||||
Reset: { svg: Reset, size: 16 },
|
||||
Settings: { svg: Settings, size: 16 },
|
||||
Trash: { svg: Trash, size: 16 },
|
||||
Unlock: { svg: Unlock, size: 16 },
|
||||
ViewModeNormal: { svg: ViewModeNormal, size: 16 },
|
||||
ViewModeOutline: { svg: ViewModeOutline, size: 16 },
|
||||
ViewModePixels: { svg: ViewModePixels, size: 16 },
|
||||
|
||||
@@ -126,6 +126,8 @@ export class FrontendNode {
|
||||
|
||||
readonly disabled!: boolean;
|
||||
|
||||
readonly locked!: boolean;
|
||||
|
||||
readonly errors!: string | undefined;
|
||||
}
|
||||
|
||||
@@ -615,6 +617,8 @@ export class LayerPanelEntry {
|
||||
|
||||
disabled!: boolean;
|
||||
|
||||
locked!: boolean;
|
||||
|
||||
parentId!: bigint | undefined;
|
||||
|
||||
id!: bigint;
|
||||
|
||||
@@ -762,6 +762,14 @@ impl JsEditorHandle {
|
||||
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
|
||||
#[wasm_bindgen(js_name = toggleLayerExpansion)]
|
||||
pub fn toggle_layer_expansion(&self, id: u64) {
|
||||
|
||||
@@ -40,6 +40,7 @@ fn add_network() -> NodeNetwork {
|
||||
imports: vec![],
|
||||
exports: vec![NodeOutput::new(NodeId(0), 0)],
|
||||
disabled: vec![],
|
||||
locked: vec![],
|
||||
previous_outputs: None,
|
||||
nodes: [DocumentNode {
|
||||
name: "Blend Image".into(),
|
||||
|
||||
@@ -526,6 +526,7 @@ pub struct NodeNetwork {
|
||||
/// Nodes that the user has disabled/hidden with the visibility eye icon.
|
||||
/// These nodes get replaced with Identity nodes during the graph flattening step.
|
||||
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.
|
||||
pub previous_outputs: Option<Vec<NodeOutput>>,
|
||||
}
|
||||
@@ -541,6 +542,7 @@ impl std::hash::Hash for NodeNetwork {
|
||||
node.hash(state);
|
||||
}
|
||||
self.disabled.hash(state);
|
||||
self.locked.hash(state);
|
||||
self.previous_outputs.hash(state);
|
||||
}
|
||||
}
|
||||
@@ -568,6 +570,7 @@ impl NodeNetwork {
|
||||
exports: vec![NodeOutput::new(NodeId(0), 0)],
|
||||
nodes: [(NodeId(0), node)].into_iter().collect(),
|
||||
disabled: vec![],
|
||||
locked: vec![],
|
||||
previous_outputs: None,
|
||||
}
|
||||
}
|
||||
@@ -816,6 +819,7 @@ impl NodeNetwork {
|
||||
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.disabled.iter_mut().for_each(|id| *id = f(*id));
|
||||
self.locked.iter_mut().for_each(|id| *id = f(*id));
|
||||
self.previous_outputs
|
||||
.iter_mut()
|
||||
.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
|
||||
self.nodes.extend(inner_network.nodes);
|
||||
self.disabled.extend(inner_network.disabled);
|
||||
self.locked.extend(inner_network.locked);
|
||||
|
||||
let mut network_offsets = HashMap::new();
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user