From fa1814a076285414ac109a6ce7885a3086e4d579 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Thu, 5 Nov 2026 05:07:47 -0500 Subject: [PATCH 1/2] =?UTF-8?q?Shortcuts:=20Ctrl+/=20opens=20Layer=20?= =?UTF-8?q?=E2=80=BA=20Lock=20Layers=E2=80=A6,=20shown=20in=20the=20Layer?= =?UTF-8?q?=20menu=20and=20the=20Layers=20panel=20menu=20(#11397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit layer.lockLayers now defaults to Cmd+/ (Ctrl+/ off the Mac) in its engine spec and in the Layer menu row. The Layers panel menu's Lock Layers… row shows the command's current key, so a key changed in Edit › Keyboard Shortcuts shows up there. Plain / still toggles Lock Transparent Pixels. Co-Authored-By: Claude Fable 5.1 --- crates/engine/src/cmd/layer_multi_cmds.rs | 2 +- crates/ui-egui/src/lib.rs | 2 + .../ui-egui/src/lock_layers_shortcut_tests.rs | 112 ++++++++++++++++++ crates/ui-egui/src/menu_catalog.rs | 2 +- crates/ui-egui/src/panels/layer_row_ui.rs | 6 +- 5 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 crates/ui-egui/src/lock_layers_shortcut_tests.rs diff --git a/crates/engine/src/cmd/layer_multi_cmds.rs b/crates/engine/src/cmd/layer_multi_cmds.rs index f85423c048..4ca3074b13 100644 --- a/crates/engine/src/cmd/layer_multi_cmds.rs +++ b/crates/engine/src/cmd/layer_multi_cmds.rs @@ -158,7 +158,7 @@ pub fn specs() -> Vec { "layer.lockLayers", "Lock Layers…", &["Layer"], - None, + Some("Cmd+/"), r##"{"transparency":bool?,"pixels":bool?,"position":bool?,"artboard":bool?,"all":bool?} (none given: toggle lock all)"##, has_layer, lock_layers diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index bfe48f9e67..3e23db915a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -778,6 +778,8 @@ mod layer_shortcut_tests; #[cfg(test)] mod live_retouch_tests; #[cfg(test)] +mod lock_layers_shortcut_tests; +#[cfg(test)] mod pencil_tests; #[cfg(test)] mod tool_options_theme_tests; diff --git a/crates/ui-egui/src/lock_layers_shortcut_tests.rs b/crates/ui-egui/src/lock_layers_shortcut_tests.rs new file mode 100644 index 0000000000..b8fff70e0e --- /dev/null +++ b/crates/ui-egui/src/lock_layers_shortcut_tests.rs @@ -0,0 +1,112 @@ +//! Ctrl+/ (Cmd+/ on the Mac) opens Layer > Lock Layers..., and the Layer menu and the Layers panel +//! menu show the command's current key. Plain / still toggles Lock Transparent Pixels (#11397). + +use egui::{Key, Modifiers}; +use serde_json::json; + +use crate::PhotocraftApp; + +fn app() -> PhotocraftApp { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 20, "height": 10})).unwrap(); + app.run("layer.new.layer", json!({})).unwrap(); + app +} + +fn press(app: &mut PhotocraftApp, key: Key, modifiers: Modifiers) { + let ctx = egui::Context::default(); + let raw = egui::RawInput { + events: vec![egui::Event::ModifiersChanged(modifiers), egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }], + ..Default::default() + }; + ctx.begin_pass(raw); + crate::shortcuts::handle(app, &ctx); + ctx.end_pass().textures_delta.clear(); +} + +fn locks(app: &PhotocraftApp) -> photocraft_doc::Locks { + let st = app.session.active().unwrap(); + st.doc.layer(st.active_layer.unwrap()).unwrap().locks +} + +fn set_shortcut(app: &mut PhotocraftApp, value: &str) { + app.session.prefs.edit(|prefs| prefs.shortcuts.insert("layer.lockLayers".into(), value.into())); +} + +#[test] +fn lock_layers_defaults_to_command_slash_in_the_engine_and_the_menu() { + assert_eq!(photocraft_engine::commands::find("layer.lockLayers").unwrap().shortcut, Some("Cmd+/")); + let row = crate::menu_catalog::CATALOG.iter().find(|c| c.3 == "layer.lockLayers").unwrap(); + assert_eq!(row.2, Some("Cmd+/")); + assert_eq!(crate::shortcuts::default_shortcut("layer.lockLayers").as_deref(), Some("Cmd+/")); +} + +#[test] +fn command_slash_runs_what_the_menu_item_runs() { + let mut via_key = app(); + press(&mut via_key, Key::Slash, Modifiers::COMMAND); + let mut via_menu = app(); + crate::menus::invoke(&mut via_menu, &egui::Context::default(), "layer.lockLayers", json!({})).unwrap(); + assert_eq!(locks(&via_key), locks(&via_menu)); + assert!(locks(&via_key).all, "Lock Layers... with no choices locks the layer"); +} + +#[test] +fn command_slash_does_nothing_when_the_menu_item_is_greyed() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + assert!(!crate::menus::is_enabled(&app, "layer.lockLayers")); + press(&mut app, Key::Slash, Modifiers::COMMAND); + assert!(app.session.active().is_none()); + assert!(app.ui.layers_panel_dialog.is_none()); +} + +#[test] +fn panel_menu_row_shows_the_current_key_and_follows_a_change() { + let mut app = app(); + let expected = crate::shortcuts::pretty("Cmd+/"); + assert_eq!(crate::shortcuts::shortcut_label(&app, "layer.lockLayers"), Some(expected)); + set_shortcut(&mut app, "Cmd+Shift+L"); + assert_eq!(crate::shortcuts::shortcut_label(&app, "layer.lockLayers"), Some(crate::shortcuts::pretty("Cmd+Shift+L"))); + // The panel menu draws with that key without trouble. + let ctx = egui::Context::default(); + ctx.begin_pass(egui::RawInput::default()); + let mut ui = egui::Ui::new( + ctx.clone(), + egui::Id::unique("lock-layers-panel-menu"), + egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(300.0, 900.0))), + ); + crate::panels::layer_row_ui::panel_menu(&mut app, &mut ui); + ctx.end_pass().textures_delta.clear(); +} + +#[test] +fn the_key_can_be_changed_and_removed() { + let mut app = app(); + set_shortcut(&mut app, "Cmd+Shift+L"); + press(&mut app, Key::Slash, Modifiers::COMMAND); + assert!(!locks(&app).all, "the old key no longer runs the command"); + press(&mut app, Key::L, Modifiers::COMMAND | Modifiers::SHIFT); + assert!(locks(&app).all, "the new key does"); + let mut app = app_with_removed_key(); + press(&mut app, Key::Slash, Modifiers::COMMAND); + assert!(!locks(&app).all); + assert_eq!(crate::shortcuts::shortcut_label(&app, "layer.lockLayers"), None); +} + +fn app_with_removed_key() -> PhotocraftApp { + let mut app = app(); + set_shortcut(&mut app, ""); + app +} + +#[test] +fn plain_slash_toggles_lock_transparency_and_command_slash_does_not() { + let mut app = app(); + press(&mut app, Key::Slash, Modifiers::NONE); + assert!(locks(&app).transparency && !locks(&app).all); + press(&mut app, Key::Slash, Modifiers::NONE); + assert!(!locks(&app).transparency); + press(&mut app, Key::Slash, Modifiers::COMMAND); + assert!(locks(&app).all); + assert!(!locks(&app).transparency, "Ctrl+/ leaves Lock Transparent Pixels alone"); +} diff --git a/crates/ui-egui/src/menu_catalog.rs b/crates/ui-egui/src/menu_catalog.rs index 328fe24fc5..b1a451c047 100644 --- a/crates/ui-egui/src/menu_catalog.rs +++ b/crates/ui-egui/src/menu_catalog.rs @@ -435,7 +435,7 @@ pub static CATALOG: &[(&[&str], &str, Option<&str>, &str)] = &[ (&["Layer", "Distribute"], "Horizontally", None, "layer.distribute.horizontally"), (&["Layer", "Distribute"], "Vertically", None, "layer.distribute.vertically"), (&["Layer"], "---", None, "---"), - (&["Layer"], "Lock Layers…", None, "layer.lockLayers"), + (&["Layer"], "Lock Layers…", Some("Cmd+/"), "layer.lockLayers"), (&["Layer"], "---", None, "---"), (&["Layer"], "Link Layers", None, "layer.linkLayers"), (&["Layer"], "Select Linked Layers", None, "layer.selectLinkedLayers"), diff --git a/crates/ui-egui/src/panels/layer_row_ui.rs b/crates/ui-egui/src/panels/layer_row_ui.rs index 82d924bacf..48e58e7010 100644 --- a/crates/ui-egui/src/panels/layer_row_ui.rs +++ b/crates/ui-egui/src/panels/layer_row_ui.rs @@ -279,7 +279,11 @@ pub fn panel_menu(app: &mut crate::PhotocraftApp, ui: &mut egui::Ui) { ui.separator(); run_menu_command(app, ui, tl!("Flatten Image"), "layer.flattenImage"); ui.separator(); - if ui.add_enabled(app.session.is_enabled("layer.lockLayers"), egui::Button::new(tl!("Lock Layers…"))).clicked() { + let mut lock_layers = egui::Button::new(tl!("Lock Layers…")); + if let Some(key) = crate::shortcuts::shortcut_label(app, "layer.lockLayers") { + lock_layers = lock_layers.shortcut_text(key); + } + if ui.add_enabled(app.session.is_enabled("layer.lockLayers"), lock_layers).clicked() { app.ui.layers_panel_dialog = Some(LayersPanelDialog::LockLayers { transparency: false, pixels: false, position: false, artboard: false, all: true }); ui.close(); } From 8fdcce3cd191e50a5ba495d54aa2137fff45c8d6 Mon Sep 17 00:00:00 2001 From: Trevor Brown Date: Thu, 5 Nov 2026 06:00:40 -0500 Subject: [PATCH 2/2] Lock Layers shortcut test: assert the key the panel menu paints (#11397) From review: the panel-menu test collected no output, so a hardcoded or missing shortcut text would have passed. It now gathers the text shapes the menu paints and checks the default key, a changed key and a removed binding. Co-Authored-By: Claude Fable 5.1 --- .../ui-egui/src/lock_layers_shortcut_tests.rs | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/crates/ui-egui/src/lock_layers_shortcut_tests.rs b/crates/ui-egui/src/lock_layers_shortcut_tests.rs index b8fff70e0e..8d5f215e19 100644 --- a/crates/ui-egui/src/lock_layers_shortcut_tests.rs +++ b/crates/ui-egui/src/lock_layers_shortcut_tests.rs @@ -60,14 +60,15 @@ fn command_slash_does_nothing_when_the_menu_item_is_greyed() { assert!(app.ui.layers_panel_dialog.is_none()); } -#[test] -fn panel_menu_row_shows_the_current_key_and_follows_a_change() { - let mut app = app(); - let expected = crate::shortcuts::pretty("Cmd+/"); - assert_eq!(crate::shortcuts::shortcut_label(&app, "layer.lockLayers"), Some(expected)); - set_shortcut(&mut app, "Cmd+Shift+L"); - assert_eq!(crate::shortcuts::shortcut_label(&app, "layer.lockLayers"), Some(crate::shortcuts::pretty("Cmd+Shift+L"))); - // The panel menu draws with that key without trouble. +/// Every text the Layers panel menu paints in one frame (the row labels and their shortcut texts). +fn painted_text(app: &mut PhotocraftApp) -> Vec { + fn collect(shape: &egui::Shape, out: &mut Vec) { + match shape { + egui::Shape::Text(text) => out.push(text.galley.job.text.clone()), + egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect(s, out)), + _ => {} + } + } let ctx = egui::Context::default(); ctx.begin_pass(egui::RawInput::default()); let mut ui = egui::Ui::new( @@ -75,8 +76,38 @@ fn panel_menu_row_shows_the_current_key_and_follows_a_change() { egui::Id::unique("lock-layers-panel-menu"), egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(300.0, 900.0))), ); - crate::panels::layer_row_ui::panel_menu(&mut app, &mut ui); - ctx.end_pass().textures_delta.clear(); + crate::panels::layer_row_ui::panel_menu(app, &mut ui); + // The texture deltas go nowhere in a test: clear them, or epaint's drop check complains. + let mut pass = ctx.end_pass(); + pass.textures_delta.clear(); + let mut out = Vec::new(); + for clipped in pass.shapes { + collect(&clipped.shape, &mut out); + } + out +} + +#[test] +fn panel_menu_row_shows_the_current_key_and_follows_a_change() { + let mut app = app(); + let default_key = crate::shortcuts::pretty("Cmd+/"); + assert_eq!(crate::shortcuts::shortcut_label(&app, "layer.lockLayers"), Some(default_key.clone())); + // The panel menu paints the key beside Lock Layers… + let texts = painted_text(&mut app); + assert!(texts.iter().any(|t| t == "Lock Layers…"), "{texts:?}"); + assert!(texts.contains(&default_key), "the default key is painted: {texts:?}"); + // A changed key is painted instead of the default. + set_shortcut(&mut app, "Cmd+Shift+L"); + let changed = crate::shortcuts::pretty("Cmd+Shift+L"); + assert_eq!(crate::shortcuts::shortcut_label(&app, "layer.lockLayers"), Some(changed.clone())); + let texts = painted_text(&mut app); + assert!(texts.contains(&changed), "{texts:?}"); + assert!(!texts.contains(&default_key), "the old key is gone: {texts:?}"); + // A removed key paints no key at all. + let mut app = app_with_removed_key(); + let texts = painted_text(&mut app); + assert!(texts.iter().any(|t| t == "Lock Layers…"), "{texts:?}"); + assert!(!texts.contains(&default_key) && !texts.contains(&changed), "{texts:?}"); } #[test]