From 712f45965b5942226f6e73f4a1648603a5f78ff4 Mon Sep 17 00:00:00 2001 From: AnhNgoo <110978888+AnhNgoo@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:07:56 +0700 Subject: [PATCH] fix(ui): share verified shortcuts between palette and ribbon --- crates/ui-egui/src/dialogs/command_search.rs | 43 +--------- crates/ui-egui/src/widgets.rs | 82 +++++++++++++------- crates/ui-egui/tests/it/dialog_focus.rs | 35 +++++++++ 3 files changed, 93 insertions(+), 67 deletions(-) diff --git a/crates/ui-egui/src/dialogs/command_search.rs b/crates/ui-egui/src/dialogs/command_search.rs index e7c59b150..81fa581e4 100644 --- a/crates/ui-egui/src/dialogs/command_search.rs +++ b/crates/ui-egui/src/dialogs/command_search.rs @@ -28,7 +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(" › "), command_search_shortcut(c.id, mac))) + .map(|c| (c.id.to_string(), c.label.to_string(), c.menu.join(" › "), crate::widgets::command_shortcut(c.id, mac))) .take(40) .collect(); @@ -69,7 +69,7 @@ pub(super) fn command_search(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dial Some(keys) => format!("{label} ({keys}) {menu}"), None => format!("{label} {menu}"), }; - let r = ui.add(egui::Button::selectable(i == d.list_index, text).truncate().min_size(vec2(scroll_area_width, 20.0))); + let r = ui.add(egui::Button::selectable(i == d.list_index, text.clone()).truncate().min_size(vec2(scroll_area_width, 20.0))); if (down || up || home || end || pgup || pgdn) && i == d.list_index && !ui.clip_rect().contains_rect(r.rect) { r.scroll_to_me(Some(if down || end || pgdn { egui::Align::Max } else { egui::Align::Min })); @@ -80,7 +80,7 @@ pub(super) fn command_search(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dial *confirm = true; } - r.on_hover_text(id); + r.on_hover_text(format!("{text}\n{id}")); } }); if ui.input(|i| i.key_pressed(Key::Enter)) { @@ -92,40 +92,3 @@ pub(super) fn command_search(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dial app.run_or_alert(&id, json!({})); } } - -/// Only advertises shortcuts verified against the grid keymap and ribbon. -/// Commands without a confirmed binding are shown without a shortcut. -fn command_search_shortcut(id: &str, mac: bool) -> Option<&'static str> { - let (mac_keys, other_keys) = match id { - "file.new" => ("⌘N", Some("Ctrl+N")), - "file.open" => ("⌘O", Some("Ctrl+O")), - "file.save" => ("⌘S", Some("Ctrl+S")), - "file.saveAs" => ("⌘⇧S", Some("Ctrl+Shift+S")), - "file.close" => ("⌘W", Some("Ctrl+W")), - "edit.copy" => ("⌘C", Some("Ctrl+C")), - "edit.cut" => ("⌘X", Some("Ctrl+X")), - "edit.paste" => ("⌘V", Some("Ctrl+V")), - "edit.undo" => ("⌘Z", Some("Ctrl+Z")), - "edit.redo" => ("⌘Y", Some("Ctrl+Y")), - "edit.selectAll" => ("⌘A", Some("Ctrl+A")), - "edit.fillDown" => ("⌘D", Some("Ctrl+D")), - "edit.fillRight" => ("⌘R", Some("Ctrl+R")), - "home.bold" => ("⌘B", Some("Ctrl+B")), - "home.italic" => ("⌘I", Some("Ctrl+I")), - "home.underline" => ("⌘U", Some("Ctrl+U")), - "formulas.autoSum" => ("⌘⇧T", Some("Alt+=")), - "insert.table" => ("⌘T", Some("Ctrl+T")), - "data.filter" => ("⌘⇧F", Some("Ctrl+Shift+L")), - "edit.flashFill" => ("Control+E", Some("Ctrl+E")), - "edit.pasteSpecial" => ("Ctrl+Cmd+V", Some("Ctrl+Alt+V")), - "home.alignLeft" => ("⌘L", None), - "home.alignCenter" => ("⌘E", None), - "selection.row" => ("Shift+Space", Some("Shift+Space")), - "selection.column" => ("Control+Space", Some("Ctrl+Space")), - "view.showObjects" => ("Ctrl+6", Some("Ctrl+6")), - "formulas.showFormulas" => ("⌘`", Some("Ctrl+`")), - _ => return None, - }; - - if mac { Some(mac_keys) } else { other_keys } -} diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 63490a36e..11ec30a23 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -15,39 +15,59 @@ fn tr<'a>(ui: &Ui, text: &'a str) -> &'a str { i18n::current(ui.ctx()).tr(text) } -/// Keyboard shortcuts shown after a ribbon tooltip's command name: Excel for Mac's on macOS, -/// Excel for Windows' elsewhere (each one GridCraft binds). Only the name is translated. -const TOOLTIP_SHORTCUTS: &[(&str, &str, &str)] = &[ - ("Save", "⌘S", "Ctrl+S"), - ("Undo", "⌘Z", "Ctrl+Z"), - ("Redo", "⌘Y", "Ctrl+Y"), - ("Paste", "⌘V", "Ctrl+V"), - ("Cut", "⌘X", "Ctrl+X"), - ("Copy", "⌘C", "Ctrl+C"), - ("Bold", "⌘B", "Ctrl+B"), - ("Italic", "⌘I", "Ctrl+I"), - ("Underline", "⌘U", "Ctrl+U"), - ("AutoSum", "⌘⇧T", "Alt+="), - ("Table", "⌘T", "Ctrl+T"), - ("Insert Link", "⌘K", "Ctrl+K"), - ("Filter", "⌘⇧F", "Ctrl+Shift+L"), - ("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+`"), +/// Verified shortcut labels shared by Ribbon tooltips and Command Palette. +/// Each entry is (command ID, optional Ribbon tooltip, macOS keys, other OS keys). +/// The dialog ID is included because Insert Link opens a UI dialog directly. +const SHORTCUTS: &[(&str, &str, &str, Option<&str>)] = &[ + ("file.new", "", "⌘N", Some("Ctrl+N")), + ("file.open", "", "⌘O", Some("Ctrl+O")), + ("file.save", "Save", "⌘S", Some("Ctrl+S")), + ("file.saveAs", "", "⌘⇧S", Some("Ctrl+Shift+S")), + ("file.close", "", "⌘W", Some("Ctrl+W")), + ("edit.copy", "Copy", "⌘C", Some("Ctrl+C")), + ("edit.cut", "Cut", "⌘X", Some("Ctrl+X")), + ("edit.paste", "Paste", "⌘V", Some("Ctrl+V")), + ("edit.undo", "Undo", "⌘Z", Some("Ctrl+Z")), + ("edit.redo", "Redo", "⌘Y", Some("Ctrl+Y")), + ("edit.selectAll", "", "⌘A", Some("Ctrl+A")), + ("edit.fillDown", "", "⌘D", Some("Ctrl+D")), + ("edit.fillRight", "", "⌘R", Some("Ctrl+R")), + ("home.bold", "Bold", "⌘B", Some("Ctrl+B")), + ("home.italic", "Italic", "⌘I", Some("Ctrl+I")), + ("home.underline", "Underline", "⌘U", Some("Ctrl+U")), + ("formulas.autoSum", "AutoSum", "⌘⇧T", Some("Alt+=")), + ("insert.table", "Table", "⌘T", Some("Ctrl+T")), + ("dialog:insertLink", "Insert Link", "⌘K", Some("Ctrl+K")), + ("data.filter", "Filter", "⌘⇧F", Some("Ctrl+Shift+L")), + ("edit.flashFill", "Flash Fill", "Control+E", Some("Ctrl+E")), + ("edit.pasteSpecial", "", "Ctrl+Cmd+V", Some("Ctrl+Alt+V")), + ("home.alignLeft", "", "⌘L", None), + ("home.alignCenter", "", "⌘E", None), + ("selection.row", "", "Shift+Space", Some("Shift+Space")), + ("selection.column", "", "Control+Space", Some("Ctrl+Space")), + ("view.showObjects", "", "Control+6", Some("Ctrl+6")), + ("formulas.showFormulas", "Show Formulas", "⌘`", Some("Ctrl+`")), ]; -/// The shortcut a ribbon tooltip shows after `tip`, for this platform (`mac`). -pub fn tooltip_shortcut(tip: &str, mac: bool) -> Option<&'static str> { - TOOLTIP_SHORTCUTS.iter().find(|(name, _, _)| *name == tip).map(|(_, m, other)| if mac { *m } else { *other }) +/// Returns a verified platform-specific shortcut for a command ID. +pub fn command_shortcut(id: &str, mac: bool) -> Option<&'static str> { + SHORTCUTS.iter().find(|(command, _, _, _)| *command == id).and_then(|(_, _, mac_keys, other)| if mac { Some(*mac_keys) } else { *other }) } +/// Returns the same shortcut for a Ribbon tooltip label. +pub fn tooltip_shortcut(tip: &str, mac: bool) -> Option<&'static str> { + SHORTCUTS.iter().find(|(_, label, _, _)| !label.is_empty() && *label == tip).and_then( + |(_, _, mac_keys, other)| { + if mac { Some(*mac_keys) } else { *other } + }, + ) +} /// A ribbon tooltip: the translated command name, then its shortcut for this platform, as Excel /// shows them ("Bold (Ctrl+B)" on Windows, "Bold (⌘B)" on macOS). fn tip_text(ui: &Ui, tip: &str) -> String { // A name shared with a two-line ribbon label (Czech "Automatický\nsoučet") reads as one line. let name = tr(ui, tip).replace('\n', " "); - let text = match tooltip_shortcut(tip, cfg!(target_os = "macos")) { + let text = match tooltip_shortcut(tip, ui.ctx().os() == egui::os::OperatingSystem::Mac) { Some(keys) => format!("{name} ({keys})"), None => name, }; @@ -714,9 +734,17 @@ mod tooltip_shortcut_tests { assert_eq!(tooltip_shortcut("Filter", true), Some("⌘⇧F")); // Search commands has no shortcut of its own (Ctrl+Shift+Space selects all). 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}: {mac}"); + for (id, tip, mac, other) in super::SHORTCUTS { + assert!(!mac.is_empty(), "{id}: empty macOS mapping"); + if let Some(keys) = other { + assert!(!keys.contains(['⌘', '⇧', '⌥']), "{id}: {keys}"); + } + assert_eq!(super::command_shortcut(id, true), Some(*mac), "{id}"); + assert_eq!(super::command_shortcut(id, false), *other, "{id}"); + if !tip.is_empty() { + assert_eq!(tooltip_shortcut(tip, true), Some(*mac), "{id}"); + assert_eq!(tooltip_shortcut(tip, false), *other, "{id}"); + } } } } diff --git a/crates/ui-egui/tests/it/dialog_focus.rs b/crates/ui-egui/tests/it/dialog_focus.rs index 878ea9bd8..de004d621 100644 --- a/crates/ui-egui/tests/it/dialog_focus.rs +++ b/crates/ui-egui/tests/it/dialog_focus.rs @@ -194,3 +194,38 @@ fn command_search_selection_shortcuts_match_runtime_os() { h.get_by_label_contains(column_label); } } + +/// Ribbon tooltips and the Command Palette read the same shortcut metadata. +#[test] +fn command_palette_and_ribbon_tooltips_share_shortcuts() { + use gridcraft_ui_egui::widgets::{command_shortcut, tooltip_shortcut}; + + for (id, tip) in [ + ("file.save", "Save"), + ("edit.undo", "Undo"), + ("edit.redo", "Redo"), + ("edit.copy", "Copy"), + ("edit.cut", "Cut"), + ("edit.paste", "Paste"), + ("home.bold", "Bold"), + ("home.italic", "Italic"), + ("home.underline", "Underline"), + ("formulas.autoSum", "AutoSum"), + ("insert.table", "Table"), + ("data.filter", "Filter"), + ("edit.flashFill", "Flash Fill"), + ("formulas.showFormulas", "Show Formulas"), + ] { + for mac in [false, true] { + assert_eq!(command_shortcut(id, mac), tooltip_shortcut(tip, mac), "{id} and {tip} diverged on mac={mac}"); + } + } + + assert_eq!(command_shortcut("edit.flashFill", true), Some("Control+E")); + assert_eq!(command_shortcut("home.alignCenter", true), Some("⌘E")); + assert_eq!(command_shortcut("home.alignCenter", false), None); + assert_eq!(command_shortcut("formulas.showFormulas", true), Some("⌘`")); + assert_eq!(command_shortcut("edit.pasteSpecial", false), Some("Ctrl+Alt+V")); + assert_eq!(command_shortcut("selection.row", false), Some("Shift+Space")); + assert_eq!(command_shortcut("unverified.command", false), None); +}