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 1c11f5db23..8ef728b71c 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -792,6 +792,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..8d5f215e19 --- /dev/null +++ b/crates/ui-egui/src/lock_layers_shortcut_tests.rs @@ -0,0 +1,143 @@ +//! 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()); +} + +/// 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( + 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(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] +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(); }