From 94f8410ccfec27a25e15b75c08cf82df04410bf0 Mon Sep 17 00:00:00 2001 From: AnhNgoo <110978888+AnhNgoo@users.noreply.github.com> Date: Mon, 2 Nov 2026 11:26:40 +0700 Subject: [PATCH] fix(ui): centralize platform-specific command shortcuts and test palette --- crates/engine/src/cmd.rs | 54 ++++++++++++- crates/engine/src/cmd/data.rs | 6 +- crates/engine/src/cmd/edit.rs | 23 +++--- crates/engine/src/cmd/format.rs | 13 ++- crates/engine/src/cmd/review.rs | 2 + crates/ui-egui/src/dialogs/command_search.rs | 18 +---- crates/ui-egui/src/widgets.rs | 6 +- crates/ui-egui/tests/it/dialog_focus.rs | 83 +++++++++++++++++--- 8 files changed, 147 insertions(+), 58 deletions(-) diff --git a/crates/engine/src/cmd.rs b/crates/engine/src/cmd.rs index ef6714eea..3bd25929e 100644 --- a/crates/engine/src/cmd.rs +++ b/crates/engine/src/cmd.rs @@ -40,6 +40,8 @@ pub struct CommandSpec { /// Ribbon/menu placement, e.g. `["Home", "Font"]`. Empty = not in menus. pub menu: &'static [&'static str], pub shortcut: Option<&'static str>, + pub shortcut_mac: Option<&'static str>, + pub shortcut_other: Option<&'static str>, pub params: &'static str, pub enabled: Enabled, pub run: Run, @@ -53,12 +55,21 @@ pub struct CommandInfo { pub label: &'static str, pub menu: Vec<&'static str>, pub shortcut: Option<&'static str>, + pub shortcut_mac: Option<&'static str>, + pub shortcut_other: Option<&'static str>, pub params: &'static str, pub enabled: bool, #[serde(skip_serializing_if = "Option::is_none")] pub disabled_reason: Option, } +impl CommandInfo { + /// Shortcut declared by the engine for the active platform. + pub fn shortcut_for_platform(&self, mac: bool) -> Option<&'static str> { + if mac { self.shortcut_mac } else { self.shortcut_other } + } +} + impl CommandSpec { pub fn info(&self, s: &Session) -> CommandInfo { let e = (self.enabled)(s); @@ -67,6 +78,8 @@ impl CommandSpec { label: self.label, menu: self.menu.to_vec(), shortcut: self.shortcut, + shortcut_mac: self.shortcut_mac, + shortcut_other: self.shortcut_other, params: self.params, enabled: e.is_ok(), disabled_reason: e.err(), @@ -116,14 +129,45 @@ pub fn has_clipboard(s: &Session) -> std::result::Result<(), String> { } macro_rules! cmd { + (platform $id:literal, $label:literal, [$($m:literal),*], $mac:expr, $other:expr, $params:literal, $en:expr, $run:expr) => { + $crate::cmd::CommandSpec { + id: $id, + label: $label, + menu: &[$($m),*], + shortcut: $mac, + shortcut_mac: $mac, + shortcut_other: $other, + params: $params, + enabled: $en, + run: $run, + journal: true, + undoable: true, + } + }; ($id:literal, $label:literal, [$($m:literal),*], $sc:expr, $params:literal, $en:expr, $run:expr) => { - $crate::cmd::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, params: $params, enabled: $en, run: $run, journal: true, undoable: true } + $crate::cmd::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, shortcut_mac: $sc, shortcut_other: $sc, params: $params, enabled: $en, run: $run, journal: true, undoable: true } }; (query $id:literal, $label:literal, [$($m:literal),*], $sc:expr, $params:literal, $en:expr, $run:expr) => { - $crate::cmd::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, params: $params, enabled: $en, run: $run, journal: false, undoable: false } + $crate::cmd::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, shortcut_mac: $sc, shortcut_other: $sc, params: $params, enabled: $en, run: $run, journal: false, undoable: false } }; (noundo $id:literal, $label:literal, [$($m:literal),*], $sc:expr, $params:literal, $en:expr, $run:expr) => { - $crate::cmd::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, params: $params, enabled: $en, run: $run, journal: true, undoable: false } + $crate::cmd::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, shortcut_mac: $sc, shortcut_other: $sc, params: $params, enabled: $en, run: $run, journal: true, undoable: false } + }; + (platform_noundo $id:literal, $label:literal, [$($m:literal),*], + $mac:expr, $other:expr, $params:literal, $en:expr, $run:expr) => { + $crate::cmd::CommandSpec { + id: $id, + label: $label, + menu: &[$($m),*], + shortcut: $mac, + shortcut_mac: $mac, + shortcut_other: $other, + params: $params, + enabled: $en, + run: $run, + journal: true, + undoable: false, + } }; } pub(crate) use cmd; @@ -178,6 +222,8 @@ pub fn register_test_command(id: &'static str, run: Run) { label: id, menu: &[], shortcut: None, + shortcut_mac: None, + shortcut_other: None, params: "", enabled: |_| Ok(()), run, @@ -195,6 +241,8 @@ pub(crate) fn register_undoable_test_command(id: &'static str, run: Run) { label: id, menu: &[], shortcut: None, + shortcut_mac: None, + shortcut_other: None, params: "", enabled: |_| Ok(()), run, diff --git a/crates/engine/src/cmd/data.rs b/crates/engine/src/cmd/data.rs index 5fa076e64..6bc8deaf3 100644 --- a/crates/engine/src/cmd/data.rs +++ b/crates/engine/src/cmd/data.rs @@ -93,16 +93,16 @@ pub fn specs() -> Vec { ), cmd!(query "data.validate", "Check Value Against Validation", [], None, "{cell?, input} → {ok, message?}", has_doc, validate_cmd), cmd!(noundo "data.circleInvalid", "Circle Invalid Data", ["Data", "Data Tools", "Data Validation"], None, "{} → invalid cells", has_doc, circle_invalid), - cmd!( + cmd!(platform "data.group", "Group", ["Data", "Outline"], - Some("Alt+Shift+Right"), + Some("Cmd+Shift+K"), Some("Alt+Shift+Right"), "{rows?: \"2:5\" | cols?: \"B:C\" | by?: rows|cols (the selection's rows or columns; a block of cells groups rows by default), sheet?}", has_doc, |s, p| group(s, p, true) ), - cmd!("data.ungroup", "Ungroup", ["Data", "Outline"], Some("Alt+Shift+Left"), "{rows? | cols? | by?: rows|cols, sheet?}", has_doc, |s, p| { + cmd!(platform "data.ungroup", "Ungroup", ["Data", "Outline"], Some("Cmd+Shift+J"), Some("Alt+Shift+Left"), "{rows? | cols? | by?: rows|cols, sheet?}", has_doc, |s, p| { group(s, p, false) }), cmd!( diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index e48fe8838..90ead5023 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -59,30 +59,29 @@ pub fn specs() -> Vec { cmd!(noundo "selection.set", "Select", [], None, "{range: \"A1:B2,D4\" | cell, active?: \"A1\", sheet?}", has_doc, selection_set), cmd!(noundo "selection.move", "Move Selection", [], None, "{dr, dc, extend?: bool, jump?: bool (Ctrl+arrow), page?: bool}", has_doc, selection_move), cmd!(noundo "selection.next", "Next Cell in Selection", [], None, "{forward?: true, byRow?: false} (Enter/Tab inside a selection)", has_doc, selection_next), - cmd!(noundo "edit.selectAll", "Select All", ["Edit"], Some("Cmd+A"), "{} (current region first, then the whole sheet)", has_doc, select_all), + cmd!(platform_noundo "edit.selectAll", "Select All", ["Edit"], Some("Cmd+A"), Some("Ctrl+A"), "{} (current region first, then the whole sheet)", has_doc, select_all), cmd!(noundo "selection.currentRegion", "Select Current Region", [], Some("Ctrl+Shift+8"), "{}", has_doc, select_region), cmd!(noundo "selection.row", "Select Entire Row", [], Some("Shift+Space"), "{}", has_doc, select_row), cmd!(noundo "selection.column", "Select Entire Column", [], Some("Ctrl+Space"), "{}", has_doc, select_col), cmd!(noundo "edit.goTo", "Go To…", ["Home", "Editing", "Find & Select"], Some("Ctrl+G"), "{reference: \"B5\" | \"Sheet2!A1:C3\" | name | \"Sheet2!Name\" | \"B14:C22,F19:G30\" | \"Price, Type\", record?: bool} (record: keep a reference without names among the recent ones) or {list: true} → {names, recent}", has_doc, go_to), cmd!(noundo "edit.goToSpecial", "Go To Special…", ["Home", "Editing", "Find & Select"], None, "{kind: blanks|constants|formulas|comments|lastCell|currentRegion|visible|conditionalFormats|dataValidation|errors|numbers|text}", has_doc, go_to_special), - cmd!(noundo "edit.undo", "Undo", ["Edit"], Some("Cmd+Z"), "{steps?: 1}", can_undo, undo), - cmd!(noundo "edit.redo", "Redo", ["Edit"], Some("Cmd+Y"), "{steps?: 1}", can_redo, redo), - cmd!(noundo "edit.copy", "Copy", ["Home", "Clipboard"], Some("Cmd+C"), "{range?, html?: bool} → {range, text, html?} (html: a styled HTML table, only when requested and small enough)", has_doc, copy), - cmd!(noundo "edit.cut", "Cut", ["Home", "Clipboard"], Some("Cmd+X"), "{range?, html?: bool} → {range, text, html?} (html: a styled HTML table, only when requested and small enough)", has_doc, cut), - cmd!( - "edit.paste", + cmd!(platform_noundo "edit.undo", "Undo", ["Edit"], Some("Cmd+Z"), Some("Ctrl+Z"), "{steps?: 1}", can_undo, undo), + cmd!(platform_noundo "edit.redo", "Redo", ["Edit"], Some("Cmd+Y"), Some("Ctrl+Y"), "{steps?: 1}", can_redo, redo), + cmd!(platform_noundo "edit.copy", "Copy", ["Home", "Clipboard"], Some("Cmd+C"), Some("Ctrl+C"), "{range?, html?: bool} → {range, text, html?} (html: a styled HTML table, only when requested and small enough)", has_doc, copy), + cmd!(platform_noundo "edit.cut", "Cut", ["Home", "Clipboard"], Some("Cmd+X"), Some("Ctrl+X"), "{range?, html?: bool} → {range, text, html?} (html: a styled HTML table, only when requested and small enough)", has_doc, cut), + cmd!(platform "edit.paste", "Paste", ["Home", "Clipboard"], - Some("Cmd+V"), + Some("Cmd+V"), Some("Ctrl+V"), "{at?: \"C3\", text?: \"tab-separated text from the system clipboard\", html?: \"the clipboard's HTML flavour; cells the source recorded as numbers (x:num, sdval) stay numbers\"}", has_doc, paste ), - cmd!( + cmd!(platform "edit.pasteSpecial", "Paste Special…", ["Home", "Clipboard"], - Some("Ctrl+Cmd+V"), + Some("Ctrl+Cmd+V"), Some("Ctrl+Alt+V"), "{what: all|formulas|values|formats|comments|validation|allExceptBorders|columnWidths|formulasAndNumberFormats|valuesAndNumberFormats, operation?: none|add|subtract|multiply|divide, skipBlanks?, transpose?, link?}", has_clipboard, paste_special @@ -97,8 +96,8 @@ pub fn specs() -> Vec { s, p, "comments" )), cmd!("edit.clearHyperlinks", "Clear Hyperlinks", ["Home", "Editing", "Clear"], None, "{range?}", has_doc, |s, p| clear(s, p, "hyperlinks")), - cmd!("edit.fillDown", "Fill Down", ["Home", "Editing", "Fill"], Some("Cmd+D"), "{range?}", has_doc, |s, p| fill_dir(s, p, 1, 0)), - cmd!("edit.fillRight", "Fill Right", ["Home", "Editing", "Fill"], Some("Cmd+R"), "{range?}", has_doc, |s, p| fill_dir(s, p, 0, 1)), + cmd!(platform "edit.fillDown", "Fill Down", ["Home", "Editing", "Fill"], Some("Cmd+D"), Some("Ctrl+D"), "{range?}", has_doc, |s, p| fill_dir(s, p, 1, 0)), + cmd!(platform "edit.fillRight", "Fill Right", ["Home", "Editing", "Fill"], Some("Cmd+R"), Some("Ctrl+R"), "{range?}", has_doc, |s, p| fill_dir(s, p, 0, 1)), cmd!("edit.fillUp", "Fill Up", ["Home", "Editing", "Fill"], None, "{range?}", has_doc, |s, p| fill_dir(s, p, -1, 0)), cmd!("edit.fillLeft", "Fill Left", ["Home", "Editing", "Fill"], None, "{range?}", has_doc, |s, p| fill_dir(s, p, 0, -1)), cmd!( diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 744b6d136..854ccc08b 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -9,23 +9,22 @@ use super::*; pub fn specs() -> Vec { vec![ // Font - cmd!("home.bold", "Bold", ["Home", "Font"], Some("Cmd+B"), "{range?, on?: bool}", has_doc, |s, p| toggle( + cmd!(platform "home.bold", "Bold", ["Home", "Font"], Some("Cmd+B"), Some("Ctrl+B"), "{range?, on?: bool}", has_doc, |s, p| toggle( s, p, |st| st.font.bold, |st, v| st.font.bold = v )), - cmd!("home.italic", "Italic", ["Home", "Font"], Some("Cmd+I"), "{range?, on?}", has_doc, |s, p| toggle( + cmd!(platform "home.italic", "Italic", ["Home", "Font"], Some("Cmd+I"), Some("Ctrl+I"), "{range?, on?}", has_doc, |s, p| toggle( s, p, |st| st.font.italic, |st, v| st.font.italic = v )), - cmd!( - "home.underline", + cmd!(platform "home.underline", "Underline", ["Home", "Font"], - Some("Cmd+U"), + Some("Cmd+U"), Some("Ctrl+U"), "{range?, on?, style?: single|double|singleAccounting|doubleAccounting}", has_doc, underline @@ -87,8 +86,8 @@ pub fn specs() -> Vec { format_cells ), // Alignment - cmd!("home.alignLeft", "Align Left", ["Home", "Alignment"], Some("Cmd+L"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Left)), - cmd!("home.alignCenter", "Center", ["Home", "Alignment"], Some("Cmd+E"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Center)), + cmd!(platform "home.alignLeft", "Align Left", ["Home", "Alignment"], Some("Cmd+L"), None, "{range?}", has_doc, |s, p| halign(s, p, HAlign::Left)), + cmd!(platform "home.alignCenter", "Center", ["Home", "Alignment"], Some("Cmd+E"), None, "{range?}", has_doc, |s, p| halign(s, p, HAlign::Center)), cmd!("home.alignRight", "Align Right", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| halign(s, p, HAlign::Right)), cmd!("home.justify", "Justify", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| halign(s, p, HAlign::Justify)), cmd!("home.alignTop", "Top Align", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| valign(s, p, VAlign::Top)), diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 9811fde9e..e1fc61efd 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -35,6 +35,8 @@ pub fn specs() -> Vec { label: "Allow Edit Ranges…", menu: &["Review", "Protect"], shortcut: None, + shortcut_mac: None, + shortcut_other: None, params: "{action?: list (default) | add | modify | delete | unlock, title?, range?, password?, newTitle?, sheet?} → ranges", enabled: has_doc, run: allow_edit_ranges, diff --git a/crates/ui-egui/src/dialogs/command_search.rs b/crates/ui-egui/src/dialogs/command_search.rs index 7a80985b3..3c75e2575 100644 --- a/crates/ui-egui/src/dialogs/command_search.rs +++ b/crates/ui-egui/src/dialogs/command_search.rs @@ -28,23 +28,7 @@ pub(super) fn command_search(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dial .into_iter() .filter(|c| c.enabled) .filter(|c| q.is_empty() || c.label.to_ascii_lowercase().contains(&q) || c.id.to_ascii_lowercase().contains(&q)) - .map(|c| { - ( - c.id.to_string(), - c.label.to_string(), - c.menu.join(" › "), - match (mac, c.id, c.shortcut) { - (false, "home.bold", Some(_)) => Some("Ctrl+B".to_owned()), - (false, "edit.pasteSpecial", Some(_)) => Some("Ctrl+Alt+V".to_owned()), - // These metadata shortcuts are only bound on macOS. - (false, "home.alignLeft" | "home.alignCenter", Some(_)) => None, - // macOS uses Command+Shift+K/J, unlike the engine's Windows metadata. - (true, "data.group", Some(_)) => Some("Cmd+Shift+K".to_owned()), - (true, "data.ungroup", Some(_)) => Some("Cmd+Shift+J".to_owned()), - (_, _, keys) => keys.map(str::to_string), - }, - ) - }) + .map(|c| (c.id.to_string(), c.label.to_string(), c.menu.join(" › "), c.shortcut_for_platform(mac).map(str::to_string))) .take(40) .collect(); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 110a1d032..2b2f40e95 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -31,7 +31,7 @@ const TOOLTIP_SHORTCUTS: &[(&str, &str, &str)] = &[ ("Table", "⌘T", "Ctrl+T"), ("Insert Link", "⌘K", "Ctrl+K"), ("Filter", "⌘⇧F", "Ctrl+Shift+L"), - ("Flash Fill", "Control+E", "Ctrl+E"), + ("Flash Fill", "⌘E", "Ctrl+E"), // The Control key, written the way each platform names it. macOS's ⌃ is supplied by its // system fonts; elsewhere the Windows spelling avoids a glyph no bundled face has. ("Show Formulas", "⌃`", "Ctrl+`"), @@ -716,7 +716,7 @@ mod tooltip_shortcut_tests { assert_eq!(tooltip_shortcut("Search commands", false), None); for (name, mac, other) in super::TOOLTIP_SHORTCUTS { assert!(!other.contains(['⌘', '⇧', '⌥']), "{name}: {other}"); - assert!(mac.starts_with('⌘') || mac.starts_with('⌃') || (*name == "Flash Fill" && *mac == "Control+E"), "{name}: {mac}"); + assert!(mac.starts_with('⌘') || mac.starts_with('⌃'), "{name}: {mac}"); } } @@ -724,7 +724,7 @@ mod tooltip_shortcut_tests { fn ribbon_tip_text_obeys_runtime_os() { use egui::os::OperatingSystem; - for (os, expected) in [(OperatingSystem::Windows, "Flash Fill (Ctrl+E)"), (OperatingSystem::Mac, "Flash Fill (Control+E)")] { + for (os, expected) in [(OperatingSystem::Windows, "Flash Fill (Ctrl+E)"), (OperatingSystem::Mac, "Flash Fill (⌘E)")] { let ctx = egui::Context::default(); ctx.set_os(os); diff --git a/crates/ui-egui/tests/it/dialog_focus.rs b/crates/ui-egui/tests/it/dialog_focus.rs index 53fb6c933..a131002e5 100644 --- a/crates/ui-egui/tests/it/dialog_focus.rs +++ b/crates/ui-egui/tests/it/dialog_focus.rs @@ -99,6 +99,11 @@ fn command_search_shows_engine_shortcuts() { for (query, expected) in [ ("home.bold", if os == OperatingSystem::Mac { "Bold (Cmd+B)" } else { "Bold (Ctrl+B)" }), ("data.filter", "Filter (Ctrl+Shift+L)"), + ("edit.copy", if os == OperatingSystem::Mac { "Copy (Cmd+C)" } else { "Copy (Ctrl+C)" }), + ("edit.paste", if os == OperatingSystem::Mac { "Paste (Cmd+V)" } else { "Paste (Ctrl+V)" }), + ("home.italic", if os == OperatingSystem::Mac { "Italic (Cmd+I)" } else { "Italic (Ctrl+I)" }), + ("home.underline", if os == OperatingSystem::Mac { "Underline (Cmd+U)" } else { "Underline (Ctrl+U)" }), + ("edit.fillDown", if os == OperatingSystem::Mac { "Fill Down (Cmd+D)" } else { "Fill Down (Ctrl+D)" }), ("edit.flashFill", "Flash Fill (Ctrl+E)"), ("home.alignLeft", if os == OperatingSystem::Mac { "Align Left (Cmd+L)" } else { "Align Left Home" }), ("home.alignCenter", if os == OperatingSystem::Mac { "Center (Cmd+E)" } else { "Center Home" }), @@ -144,22 +149,36 @@ fn command_palette_shortcuts_come_from_engine_metadata() { let session = gridcraft_engine::Session::new(); let commands = session.commands(); - for (id, expected) in [ - ("home.bold", "Cmd+B"), - ("data.filter", "Ctrl+Shift+L"), - ("edit.flashFill", "Ctrl+E"), - ("edit.pasteSpecial", "Ctrl+Cmd+V"), - ("selection.row", "Shift+Space"), - ("selection.column", "Ctrl+Space"), - ("selection.currentRegion", "Ctrl+Shift+8"), - ("home.alignLeft", "Cmd+L"), - ("home.alignCenter", "Cmd+E"), - ("data.group", "Alt+Shift+Right"), - ("data.ungroup", "Alt+Shift+Left"), + for (id, mac, other) in [ + ("home.bold", Some("Cmd+B"), Some("Ctrl+B")), + ("edit.selectAll", Some("Cmd+A"), Some("Ctrl+A")), + ("edit.undo", Some("Cmd+Z"), Some("Ctrl+Z")), + ("edit.redo", Some("Cmd+Y"), Some("Ctrl+Y")), + ("edit.copy", Some("Cmd+C"), Some("Ctrl+C")), + ("edit.cut", Some("Cmd+X"), Some("Ctrl+X")), + ("edit.paste", Some("Cmd+V"), Some("Ctrl+V")), + ("edit.fillDown", Some("Cmd+D"), Some("Ctrl+D")), + ("edit.fillRight", Some("Cmd+R"), Some("Ctrl+R")), + ("home.italic", Some("Cmd+I"), Some("Ctrl+I")), + ("home.underline", Some("Cmd+U"), Some("Ctrl+U")), + ("edit.pasteSpecial", Some("Ctrl+Cmd+V"), Some("Ctrl+Alt+V")), + ("home.alignLeft", Some("Cmd+L"), None), + ("home.alignCenter", Some("Cmd+E"), None), + ("data.group", Some("Cmd+Shift+K"), Some("Alt+Shift+Right")), + ("data.ungroup", Some("Cmd+Shift+J"), Some("Alt+Shift+Left")), + ("edit.flashFill", Some("Ctrl+E"), Some("Ctrl+E")), + ("data.filter", Some("Ctrl+Shift+L"), Some("Ctrl+Shift+L")), + ("selection.row", Some("Shift+Space"), Some("Shift+Space")), + ("selection.column", Some("Ctrl+Space"), Some("Ctrl+Space")), ] { let command = commands.iter().find(|c| c.id == id).unwrap(); - assert_eq!(command.shortcut, Some(expected), "{id}"); + assert_eq!(command.shortcut_for_platform(true), mac, "{id}: macOS"); + assert_eq!(command.shortcut_for_platform(false), other, "{id}: Windows/Linux"); } + + let command = commands.iter().find(|c| c.id == "home.alignRight").unwrap(); + assert_eq!(command.shortcut_for_platform(true), None); + assert_eq!(command.shortcut_for_platform(false), None); } /// Current Region displays the shortcut declared in engine metadata. #[test] @@ -182,3 +201,41 @@ fn command_search_shows_current_region_shortcut() { h.get_by_label_contains(expected); } } + +/// Disabled commands are absent; commands without shortcuts show no invented keys. +#[test] +fn command_search_hides_disabled_and_handles_missing_shortcuts() { + use egui::os::OperatingSystem; + + for os in [OperatingSystem::Windows, OperatingSystem::Mac] { + let mut h = harness(); + h.ctx.set_os(os); + h.run_steps(2); + + // Undo is disabled in a fresh workbook with no undo history. + let undo = h.state().session.commands().into_iter().find(|c| c.id == "edit.undo").unwrap(); + + assert!(!undo.enabled, "Undo should initially be disabled"); + + h.state_mut().open_dialog("commandSearch", json!({})); + h.run_steps(6); + + h.state_mut().dialog.as_mut().unwrap().search = "edit.undo".into(); + h.run_steps(3); + + assert!( + h.query_by_label("Undo (Cmd+Z) Edit").is_none() && h.query_by_label("Undo (Ctrl+Z) Edit").is_none(), + "Disabled Undo must not appear in the palette" + ); + + // Align Right has no shortcut on either platform. + h.state_mut().dialog.as_mut().unwrap().search = "home.alignRight".into(); + h.run_steps(3); + + let align = h.state().session.commands().into_iter().find(|c| c.id == "home.alignRight").unwrap(); + + assert_eq!(align.shortcut_for_platform(os == OperatingSystem::Mac), None); + + h.get_by_label_contains("Align Right Home"); + } +}