fix(ui): share verified shortcuts between palette and ribbon

This commit is contained in:
AnhNgoo
2026-10-28 10:07:56 +07:00
parent bf4a3969e8
commit 712f45965b
3 changed files with 93 additions and 67 deletions
+3 -40
View File
@@ -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 }
}
+55 -27
View File
@@ -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}");
}
}
}
}
+35
View File
@@ -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);
}