diff --git a/crates/engine/src/cmd/data.rs b/crates/engine/src/cmd/data.rs index 1c4c778..7a4f0c9 100644 --- a/crates/engine/src/cmd/data.rs +++ b/crates/engine/src/cmd/data.rs @@ -29,7 +29,7 @@ pub fn specs() -> Vec { "data.filter", "Filter", ["Data", "Sort & Filter"], - Some("Cmd+Shift+F"), + Some("Ctrl+Shift+L"), "{range?} toggles AutoFilter on the current region", has_doc, toggle_filter diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index ce34f44..fb00563 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -92,7 +92,7 @@ pub fn specs() -> Vec { has_doc, fill_series ), - cmd!("edit.flashFill", "Flash Fill", ["Home", "Editing", "Fill"], Some("Cmd+E"), "{range?}", has_doc, flash_fill), + cmd!("edit.flashFill", "Flash Fill", ["Home", "Editing", "Fill"], Some("Ctrl+E"), "{range?}", has_doc, flash_fill), cmd!(noundo "edit.find", "Find…", ["Home", "Editing", "Find & Select"], Some("Cmd+F"), "{what, matchCase?, wholeCell?, lookIn?: formulas|values, byColumns?, all?: bool, within?: sheet|workbook}", has_doc, find), cmd!( "edit.replace", diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index 2b6e50b..be09ba2 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -89,7 +89,7 @@ pub fn specs() -> Vec { // 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!("home.alignRight", "Align Right", ["Home", "Alignment"], Some("Cmd+R"), "{range?}", has_doc, |s, p| halign(s, p, HAlign::Right)), + 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)), cmd!("home.alignMiddle", "Middle Align", ["Home", "Alignment"], None, "{range?}", has_doc, |s, p| valign(s, p, VAlign::Center)), diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index 22f9bdd..6077483 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -157,8 +157,10 @@ pub fn specs() -> Vec { has_doc, insert_link ), - cmd!("review.newNote", "New Note", ["Review", "Notes"], None, "{cell?, text, author?}", has_doc, |s, p| comment(s, p, false)), - cmd!("review.newComment", "New Comment", ["Review", "Comments"], None, "{cell?, text, author?}", has_doc, |s, p| comment(s, p, true)), + cmd!("review.newNote", "New Note", ["Review", "Notes"], Some("Shift+F2"), "{cell?, text, author?}", has_doc, |s, p| comment(s, p, false)), + cmd!("review.newComment", "New Comment", ["Review", "Comments"], Some("Cmd+Shift+F2"), "{cell?, text, author?}", has_doc, |s, p| comment( + s, p, true + )), cmd!("review.replyComment", "Reply", [], None, "{cell?, text, author?}", has_doc, reply_comment), cmd!("review.resolveComment", "Resolve Thread", [], None, "{cell?, resolved?: true}", has_doc, resolve_comment), cmd!("review.deleteComment", "Delete Comment", ["Review", "Comments"], None, "{cell?}", has_doc, delete_comment), diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index beb126a..fa3f94b 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -1949,8 +1949,11 @@ fn keyboard(app: &mut SheetApp, ctx: &egui::Context, resp: &egui::Response, geo: return; } } - egui::Event::Key { key, pressed: true, modifiers: m, .. } => { + egui::Event::Key { key, physical_key, pressed: true, modifiers: m, .. } => { use egui::Key; + if crate::keymap::dispatch(app, ctx, key, physical_key, m, geo, sh) { + continue; + } let cmd = m.command; let shift = m.shift; let mv = |app: &mut SheetApp, dr: i64, dc: i64| { diff --git a/crates/ui-egui/src/keymap.rs b/crates/ui-egui/src/keymap.rs new file mode 100644 index 0000000..1dc90e4 --- /dev/null +++ b/crates/ui-egui/src/keymap.rs @@ -0,0 +1,239 @@ +//! Excel keyboard shortcuts beyond the plain Ctrl/Cmd letters in [`crate::ribbon::shortcut`]: +//! number-format and border combinations, F-keys, Alt/Option combinations and the Excel for Mac +//! variants. Every entry maps to an existing command or dialog. `docs/ui-parity.md` holds the +//! audit against Microsoft's published Windows and Mac shortcut lists. + +use egui::{Key, Modifiers}; +use gridcraft_engine::core::{CellRef, RangeRef}; +use gridcraft_engine::model::Sheet; +use serde_json::{Value as Json, json}; + +use crate::SheetApp; +use crate::grid::Geo; + +/// What a shortcut does. +#[derive(Clone, Debug, PartialEq)] +pub enum Shortcut { + /// Run a command with these parameters. + Run(&'static str, Json), + /// Open a dialog. + Dialog(&'static str, Json), + /// Start editing with the current date or time (Ctrl+; and Ctrl+Shift+;). + Date, + Time, + /// Ctrl+' / Ctrl+Shift+": the formula / value of the cell above, ready to edit. + FormulaAbove, + ValueAbove, + /// Extend the selection to A1, to the last used cell, or to the start of the row. + ExtendToStart, + ExtendToEnd, + ExtendToRowStart, + /// Alt+Page Down / Alt+Page Up: one screen right / left. + ScreenRight, + ScreenLeft, + /// Excel for Mac Control+U: edit the active cell (like F2). + Edit, + /// Shift+F10: the cell context menu. + ContextMenu, + /// Alt+Down: the active cell's drop-down list. + DropDown, + /// Ctrl+Alt+= / Ctrl+Alt+-: zoom in / out. + Zoom(i32), +} + +fn run(id: &'static str) -> Shortcut { + Shortcut::Run(id, json!({})) +} + +fn number_format(code: &str) -> Shortcut { + Shortcut::Run("home.numberFormat", json!({"code": code})) +} + +fn borders(preset: &str) -> Shortcut { + Shortcut::Run("home.borders", json!({"preset": preset})) +} + +/// The shortcut for a key press in the grid, if any. `physical` is the key's position on a US +/// layout (as Excel matches Ctrl+Shift+4 rather than the `$` character); `mac` selects the Excel +/// for Mac variants where they differ. +pub fn lookup(key: Key, physical: Option, m: Modifiers, mac: bool) -> Option { + use Key::*; + let k = physical.unwrap_or(key); + // Control itself (Excel for Mac also uses Control for these), Ctrl on Windows / Cmd on Mac, + // and Alt/Option alone. + let ctrl = m.ctrl && !m.alt; + let cmd = m.command && !m.alt; + let alt = m.alt && !m.ctrl && !m.command; + let ctrl_alt = m.alt && (m.ctrl || m.command); + let plain = !m.ctrl && !m.command && !m.alt; + let s = m.shift; + Some(match k { + // Ctrl+Shift+~ ! @ # $ % ^ & _ : number formats and borders (Windows and Mac). + Backtick if ctrl && s => Shortcut::Run("home.numberFormat", json!({"format": "General"})), + Num1 if ctrl && s => number_format("#,##0.00"), + Num2 if ctrl && s => number_format("h:mm AM/PM"), + Num3 if ctrl && s => number_format("d-mmm-yy"), + Num4 if ctrl && s => number_format("$#,##0.00_);($#,##0.00)"), + Num5 if ctrl && s => number_format("0%"), + Num6 if ctrl && s => number_format("0.00E+00"), + Num7 if ctrl && s => borders("outside"), + Minus if ctrl && s => borders("none"), + Num8 if ctrl && s => run("selection.currentRegion"), + Semicolon if ctrl && s => Shortcut::Time, + Quote if ctrl && s => Shortcut::ValueAbove, + Quote if ctrl => Shortcut::FormulaAbove, + // Windows Ctrl+; is in `ribbon::shortcut`; Excel for Mac uses Control+; and Command+;. + Semicolon if mac && ctrl => Shortcut::Date, + Semicolon if mac && cmd && !s => Shortcut::Time, + // Ctrl+2..5 (Windows). + Num2 if ctrl && !mac => run("home.bold"), + Num3 if ctrl && !mac => run("home.italic"), + Num4 if ctrl && !mac => run("home.underline"), + Num5 if ctrl && !mac => run("home.strikethrough"), + // Letters that differ between Windows and Mac. + L if (cmd || ctrl) && s => run("data.filter"), + L if cmd && mac => run("home.alignLeft"), + L if ctrl && mac => Shortcut::Dialog("defineName", json!({})), + U if ctrl && mac && !s => Shortcut::Edit, + L if cmd => run("insert.table"), + E if cmd && mac && !s => run("home.alignCenter"), + E if ctrl && mac && !s => run("edit.flashFill"), + F if cmd && s && !mac => Shortcut::Dialog("formatCells", json!({"tab": "Font"})), + G if cmd && s && !mac => Shortcut::Dialog("statistics", json!({})), + H if cmd && s && mac => Shortcut::Dialog("find", json!({"replace": true})), + R if cmd && s && mac => Shortcut::Dialog("sort", json!({})), + K if cmd && s && mac => run("data.group"), + J if cmd && s && mac => run("data.ungroup"), + P if cmd && !s => run("file.print"), + Period if cmd && s => run("home.increaseFontSize"), + Comma if cmd && s => run("home.decreaseFontSize"), + Space if cmd && s => run("edit.selectAll"), + Home if cmd && s => Shortcut::ExtendToStart, + End if cmd && s => Shortcut::ExtendToEnd, + Home if plain && s => Shortcut::ExtendToRowStart, + // Ctrl+Alt / Cmd+Option combinations. + V if ctrl_alt || (mac && m.ctrl && m.command) => Shortcut::Dialog("pasteSpecial", json!({})), + Equals | Plus if ctrl_alt => Shortcut::Zoom(10), + Minus if ctrl_alt && !mac => Shortcut::Zoom(-10), + F9 if ctrl_alt => run("formulas.calculateNow"), + R if ctrl_alt && mac => run("view.collapseRibbon"), + Num0 if ctrl_alt && mac => borders("outside"), + Minus if ctrl_alt && mac => borders("none"), + ArrowLeft if ctrl_alt && mac => borders("left"), + ArrowRight if ctrl_alt && mac => borders("right"), + ArrowUp if ctrl_alt && mac => borders("top"), + ArrowDown if ctrl_alt && mac => borders("bottom"), + // Alt / Option alone. + Equals if alt && !s && !mac => run("formulas.autoSum"), + ArrowDown if alt && !s => Shortcut::DropDown, + PageDown if alt => Shortcut::ScreenRight, + PageUp if alt => Shortcut::ScreenLeft, + ArrowRight if alt && s && !mac => run("data.group"), + ArrowLeft if alt && s && !mac => run("data.ungroup"), + ArrowRight if alt && !s && mac => run("sheet.next"), + ArrowLeft if alt && !s && mac => run("sheet.previous"), + F1 if alt && s => run("home.insertSheet"), + F1 if alt => run("insert.chart"), + F10 if alt && s => Shortcut::Dialog("errorChecking", json!({})), + // Function keys. + F1 if cmd && !s => run("view.collapseRibbon"), + F2 if (cmd || ctrl) && s => Shortcut::Dialog("comment", json!({"threaded": true})), + F2 if plain && s => Shortcut::Dialog("comment", json!({"threaded": false})), + F3 if cmd && s => run("formulas.createFromSelection"), + F3 if cmd && mac => Shortcut::Dialog("defineName", json!({})), + F3 if cmd => Shortcut::Dialog("nameManager", json!({})), + F3 if plain && s => Shortcut::Dialog("insertFunction", json!({})), + F4 if cmd && !s => run("file.close"), + F5 if plain && s => Shortcut::Dialog("find", json!({})), + F5 if plain => Shortcut::Dialog("goTo", json!({})), + F7 if plain && !s => Shortcut::Dialog("spelling", json!({})), + F9 if plain && s => run("formulas.calculateSheet"), + F10 if plain && s => Shortcut::ContextMenu, + F11 if plain && s => run("home.insertSheet"), + F12 if plain && !s => run("file.saveAs"), + _ => return None, + }) +} + +/// Handles a key press in the grid. Returns whether it was a shortcut. +pub(crate) fn dispatch(app: &mut SheetApp, ctx: &egui::Context, key: Key, physical: Option, m: Modifiers, geo: &Geo, sh: &Sheet) -> bool { + let mac = ctx.os() == egui::os::OperatingSystem::Mac; + // AltGr arrives as Ctrl+Alt on Windows: a key that also typed a character (AltGr+= is `~` + // on a German layout) is text, not a shortcut. + if !mac && m.alt && m.ctrl && ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Text(_)))) { + return false; + } + let Some(shortcut) = lookup(key, physical, m, mac) else { return false }; + let (active, anchor) = app.session.active().map(|d| (d.selection.active, d.selection.anchor)).unwrap_or_default(); + match shortcut { + Shortcut::Run(id, params) => { + app.run_or_alert(id, params); + app.grid.ensure_visible = true; + } + Shortcut::Dialog(name, params) => app.open_dialog(name, params), + Shortcut::Date | Shortcut::Time => { + let now = gridcraft_engine::calc::now_serial(); + let (value, format) = if shortcut == Shortcut::Date { (now.floor(), "m/d/yyyy") } else { (now.fract(), "h:mm AM/PM") }; + let text = app + .session + .active() + .map(|d| gridcraft_engine::display::format(&gridcraft_engine::core::Value::Number(value), format, &d.wb).text) + .unwrap_or_default(); + edit_with(app, text); + } + Shortcut::FormulaAbove | Shortcut::ValueAbove => { + let Some(above) = active.row.checked_sub(1).map(|r| CellRef::new(r, active.col)) else { return true }; + let text = if shortcut == Shortcut::FormulaAbove { + sh.cell(above).map(|c| c.input_text()).unwrap_or_default() + } else { + app.session + .active() + .and_then(|d| sh.value_ref(above).map(|v| gridcraft_engine::display::format(v, "General", &d.wb).text)) + .unwrap_or_default() + }; + edit_with(app, text); + } + Shortcut::ExtendToStart => extend(app, anchor, active, CellRef::new(0, 0)), + Shortcut::ExtendToEnd => { + let end = sh.used_range().map(|u| u.end).unwrap_or_default(); + extend(app, anchor, active, end); + } + Shortcut::ExtendToRowStart => extend(app, anchor, active, CellRef::new(active.row, 0)), + Shortcut::ScreenRight | Shortcut::ScreenLeft => { + let width = ((geo.cells.width() - geo.frozen_w) / geo.z).max(1.0); + let cols = (width / sh.default_col_width.max(1.0)).floor().max(1.0) as i64; + let dc = if shortcut == Shortcut::ScreenRight { cols } else { -cols }; + let _ = app.session.run("selection.move", json!({"dr": 0, "dc": dc, "extend": m.shift})); + app.grid.ensure_visible = true; + } + Shortcut::Edit => app.begin_edit(None, false), + Shortcut::ContextMenu => app.grid.context_menu = Some(geo.cell_rect(sh, active).center()), + Shortcut::DropDown => { + if sh.validations.iter().any(|d| d.in_cell_dropdown && d.ranges.iter().any(|r| r.contains(active))) { + app.grid.list_picker = Some(active); + } + } + Shortcut::Zoom(step) => { + let percent = (i32::from(sh.zoom).saturating_add(step)).clamp(10, 400); + app.run_or_alert("view.zoom", json!({"percent": percent})); + } + } + true +} + +/// Starts editing the active cell with `text` and the caret at its end. +fn edit_with(app: &mut SheetApp, text: String) { + app.begin_edit(Some(text), false); + if let Some(ed) = app.editor.as_mut() { + ed.caret = ed.text.chars().count(); + } +} + +/// Selects `anchor`..`to`, keeping the anchor and the active cell. +fn extend(app: &mut SheetApp, anchor: CellRef, active: CellRef, to: CellRef) { + let _ = app.session.run("selection.set", json!({"range": RangeRef::new(anchor, to).a1(), "active": active.a1()})); + if let Some(d) = app.session.active_mut() { + d.selection.anchor = anchor; + } + app.grid.ensure_visible = true; +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e6ddc61..437cfa1 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -18,6 +18,7 @@ mod formula_locale; pub mod grid; pub mod i18n; pub mod icons; +pub mod keymap; pub mod keytips; pub mod panes; pub mod pivot_pane; diff --git a/crates/ui-egui/tests/keyboard_shortcuts.rs b/crates/ui-egui/tests/keyboard_shortcuts.rs new file mode 100644 index 0000000..a06f918 --- /dev/null +++ b/crates/ui-egui/tests/keyboard_shortcuts.rs @@ -0,0 +1,108 @@ +//! Excel keyboard shortcuts (crate::keymap) dispatched through real egui key events. + +use egui::{Event, Key, Modifiers}; +use gridcraft_engine::Session; +use gridcraft_engine::core::CellRef; +use gridcraft_engine::model::BorderStyle; +use gridcraft_ui_egui::SheetApp; +use gridcraft_ui_egui::keymap::{Shortcut, lookup}; +use serde_json::json; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +fn harness(session: Session) -> Harness { + let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + h +} + +fn book(selected: &str) -> Session { + let mut s = Session::new(); + s.new_workbook(); + s.execute("range.setValues", json!({"range": "A1", "values": [[1.5, 2], [3, 4], [5, 6]]})).unwrap(); + s.execute("cell.set", json!({"cell": "C1", "input": "=A1*2"})).unwrap(); + s.execute("selection.set", json!({"range": selected})).unwrap(); + s +} + +/// A key press as a US keyboard reports it (the physical key is the unshifted one). +fn key(h: &mut Harness, k: Key, m: Modifiers) { + h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: true, repeat: false, modifiers: m }); + h.input_mut().events.push(Event::Key { key: k, physical_key: Some(k), pressed: false, repeat: false, modifiers: m }); + h.run_steps(2); +} + +fn style(h: &Harness, a1: &str) -> gridcraft_engine::model::Style { + let d = h.state().session.active().unwrap(); + let sh = d.wb.active().unwrap(); + d.wb.styles.get(sh.style_id(CellRef::parse(a1).unwrap())).clone() +} + +/// Ctrl on Windows and Linux sets both `ctrl` and `command`. +const CTRL: Modifiers = Modifiers { alt: false, ctrl: true, shift: false, mac_cmd: false, command: true }; +const CTRL_SHIFT: Modifiers = Modifiers { shift: true, ..CTRL }; + +#[test] +fn ctrl_shift_symbols_apply_number_formats_and_borders() { + let mut h = harness(book("A1:B2")); + key(&mut h, Key::Num4, CTRL_SHIFT); + assert_eq!(style(&h, "B2").num_fmt.as_str(), "$#,##0.00_);($#,##0.00)"); + key(&mut h, Key::Num5, CTRL_SHIFT); + assert_eq!(style(&h, "A1").num_fmt.as_str(), "0%"); + key(&mut h, Key::Backtick, CTRL_SHIFT); + assert_eq!(style(&h, "A1").num_fmt.as_str(), "General"); + key(&mut h, Key::Num7, CTRL_SHIFT); + assert!(style(&h, "A1").borders.top.style != BorderStyle::None && style(&h, "B2").borders.right.style != BorderStyle::None); + key(&mut h, Key::Minus, CTRL_SHIFT); + assert!(style(&h, "A1").borders.top.style == BorderStyle::None); + // Ctrl+Shift+End extends from the anchor to the last used cell. + key(&mut h, Key::End, CTRL_SHIFT); + assert_eq!(h.state().session.active().unwrap().selection.current().a1(), "A1:C3"); +} + +#[test] +fn copy_formula_from_above_and_function_keys() { + let mut h = harness(book("C2")); + key(&mut h, Key::Quote, CTRL); + assert_eq!(h.state().editor.as_ref().map(|e| e.text.clone()).as_deref(), Some("=A1*2")); + key(&mut h, Key::Escape, Modifiers::NONE); + assert!(h.state().editor.is_none()); + let sheets = h.state().session.active().unwrap().wb.sheets.len(); + key(&mut h, Key::F11, Modifiers::SHIFT); + assert_eq!(h.state().session.active().unwrap().wb.sheets.len(), sheets + 1); + // Back on the data: Alt+F1 charts the current region, Ctrl+Alt+= zooms in. + h.state_mut().session.run("sheet.previous", json!({})).unwrap(); + h.state_mut().session.run("selection.set", json!({"range": "A1:B3"})).unwrap(); + key(&mut h, Key::F1, Modifiers::ALT); + assert_eq!(h.state().session.active().unwrap().wb.sheets[0].charts.len(), 1); + key(&mut h, Key::Equals, Modifiers { alt: true, ..CTRL }); + assert_eq!(h.state().session.active().unwrap().wb.sheets[0].zoom, 110); +} + +#[test] +fn windows_and_mac_variants() { + let cmd = CTRL; + let mac_cmd = Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE }; + let ctrl = Modifiers::CTRL; + let run = |id: &'static str| Some(Shortcut::Run(id, json!({}))); + assert_eq!(lookup(Key::L, Some(Key::L), cmd, false), run("insert.table")); + assert_eq!(lookup(Key::L, Some(Key::L), mac_cmd, true), run("home.alignLeft")); + assert_eq!(lookup(Key::E, Some(Key::E), mac_cmd, true), run("home.alignCenter")); + assert_eq!(lookup(Key::E, Some(Key::E), ctrl, true), run("edit.flashFill")); + assert_eq!(lookup(Key::Equals, Some(Key::Equals), Modifiers::ALT, false), run("formulas.autoSum")); + // The shifted character doesn't matter: Ctrl+Shift+1 is the Number format wherever `!` is. + assert_eq!( + lookup(Key::Exclamationmark, Some(Key::Num1), CTRL_SHIFT, false), + Some(Shortcut::Run("home.numberFormat", json!({"code": "#,##0.00"}))) + ); + // Plain keys stay with the grid (typing, navigation). + assert_eq!(lookup(Key::A, Some(Key::A), Modifiers::NONE, false), None); + assert_eq!(lookup(Key::F2, Some(Key::F2), Modifiers::NONE, false), None); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index f06c8c5..96eb5b5 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -57,10 +57,62 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes | F2, F4, F5, F9 family, Shift+F3, F11 | yes | most; F11 chart sheet missing (no chart sheets) | | Mac-specific (Cmd variants, Fn keys) | yes | Cmd mapping done | | Delete selected charts, pictures and shapes | yes | done (#213) | +| Shortcuts bound to commands | 200+ documented shortcuts per platform | audited below (#110): shortcuts whose feature exists are bound, with the exceptions listed; the rest wait for their features | +| Ctrl+arrow, Ctrl+Shift+arrow, Ctrl+Home/End, Ctrl+Shift+Home/End, Page Up/Down, Alt+Page | yes | done | +| Ctrl+1, Ctrl+; and Ctrl+Shift+; , Ctrl+D/R, Ctrl+E, Ctrl+T/L, Ctrl+K, Ctrl+Shift+L, Ctrl+Shift+~!@#$%^&_ | yes | done | +| F2, F4, F5, F7, F9 family, Shift+F2/F3/F10/F11, F12 | yes | done; F11 chart sheet, F8 extend mode, F3 Paste Name, F4 Repeat missing | +| Mac-specific (Cmd variants, Control variants, Fn keys) | yes | done where a feature exists (`keymap.rs` picks the Mac variant) | | Requests | — | #110 (more shortcuts and hot keys), #33 (sequential Alt; initial support landed in #43) | -Work: a shortcut table generated from the command catalog checked against Microsoft's published -shortcut lists for Windows and Mac, then fill the gaps: 6–10 h. +### Shortcut audit (#110) + +Checked against Microsoft's published "Keyboard shortcuts in Excel" (Windows and Mac tables, +2026-10-11). Alt key-tip sequences are tracked separately (#33). Bindings live in +`crates/ui-egui/src/grid.rs` (navigation, editing keys), `ribbon::shortcut` (Ctrl/Cmd letters), +`keymap.rs` (everything added in #110, with a `lookup` table that tests cover) and +`formula_bar.rs` (keys while editing). "Ctrl" means Cmd on a Mac unless the Mac column says +otherwise. Number-format and border keys match the physical key (Ctrl+Shift+4, wherever `$` is). + +| Shortcut (Windows) | Mac | Action | Status | +|---|---|---|---| +| Ctrl+C / X / V, Ctrl+Z / Y, Ctrl+A, Ctrl+B / I / U | same with Cmd | clipboard, undo/redo, select all, bold/italic/underline | present | +| Ctrl+S, Ctrl+Shift+S, Ctrl+O, Ctrl+N, Ctrl+W | Cmd | save, save as, open, new, close | present | +| Ctrl+F, Ctrl+H, Ctrl+G, Ctrl+K, Ctrl+T | Cmd (Ctrl+H) | find, replace, go to, hyperlink, table | present | +| Ctrl+D, Ctrl+R, Ctrl+E, Ctrl+1, Ctrl+9 / 0, Ctrl+Shift+9 / 0 | Cmd (Ctrl+E on Mac: see below) | fill down/right, Flash Fill, Format Cells, hide/unhide | present | +| Ctrl+- , Ctrl+Shift+= | Cmd | Delete / Insert cells dialog | present | +| Ctrl+; , Ctrl+` | Control+; | current date, show formulas | present (Mac Control+; added) | +| Arrows, Shift / Ctrl / Ctrl+Shift+arrows, Home, Ctrl+Home / End, Page Up / Down, Ctrl+Page Up / Down | Cmd+arrows | navigation and extension | present | +| Tab, Shift+Tab, Enter, Shift+Enter, Ctrl+Space, Shift+Space, Delete, Esc | same | entry and selection | present | +| F2, F9, F10 (key tips), F4 / Alt+Enter / Ctrl+Enter / Ctrl+Shift+Enter while editing | same | edit, calculate, references, new line, fill, array | present | +| Ctrl+Shift+~ ! @ # $ % ^ | Control+Shift | General, Number, Time, Date, Currency, Percent, Scientific | **added** | +| Ctrl+Shift+& / Ctrl+Shift+_ | Cmd+Option+0 / Cmd+Option+- (and Cmd+Option+arrows for one side) | outline border / remove borders | **added** | +| Ctrl+2 / 3 / 4 / 5 | — | bold / italic / underline / strikethrough | **added** | +| Ctrl+Shift+: | Cmd+; | current time | **added** | +| Ctrl+' / Ctrl+Shift+" | Control | formula / value from the cell above | **added** | +| Ctrl+L | Cmd+L is Align Left, Cmd+E Center, Control+E Flash Fill | create table | **added** | +| Ctrl+Shift+L | Cmd+Shift+F (present) / Control+Shift+L | filter | **added** | +| Ctrl+Shift+F, Ctrl+Shift+G | — | Format Cells Font tab, Workbook Statistics | **added** | +| — | Cmd+Shift+H, Cmd+Shift+R, Cmd+Shift+K / J, Control+L, Control+U | replace, sort, group / ungroup, Define Name, edit cell | **added** | +| Alt+Shift+Right / Left | Option+Right / Left is next/previous sheet | group / ungroup | **added** | +| Ctrl+Shift+* (Ctrl+Shift+8), Ctrl+Shift+Space | Cmd+Shift+Space | current region, select all | **added** | +| Ctrl+Shift+Home / End, Shift+Home | same | extend to A1 / last used cell / row start | **added** | +| Ctrl+P | Cmd+P | print | **added** | +| Ctrl+Shift+> / < | Cmd+Shift+> / < | font size | **added** | +| Ctrl+Alt+V | Cmd+Control+V, Cmd+Option+V | Paste Special | **added** | +| Ctrl+Alt+= / Ctrl+Alt+- | — | zoom in / out (10% steps) | **added** | +| Alt+=, Alt+Down, Alt+Page Down / Up | Option+Down, Option+Page Down / Up | AutoSum, drop-down list, one screen right / left | **added** | +| Alt+F1, Alt+Shift+F1, Shift+F11 | Shift+F11 | embedded chart, new sheet | **added** | +| Shift+F2, Ctrl+Shift+F2 | Shift+F2, Cmd+Shift+F2 | note, threaded comment | **added** | +| Shift+F3, Ctrl+F3, Ctrl+Shift+F3 | Shift+F3, Cmd+F3 (Define Name), Cmd+Shift+F3 | Insert Function, Name Manager, Create from Selection | **added** | +| F5, Shift+F5 (Mac), F7, Shift+F9, Ctrl+Alt+F9, F12, Ctrl+F4 | same | Go To, Find, Spelling, calculate sheet / all, Save As, close | **added** | +| Shift+F10, Alt+Shift+F10, Ctrl+F1 | Shift+F10, Cmd+Option+R | context menu, Error Checking, ribbon | **added** | +| F1, F3, F4 (repeat), F6 / Shift+F6, F8 / Shift+F8, F11, End mode | same | help, Paste Name, Repeat, pane cycling, extend/add mode, chart sheet | missing (no such feature yet) | +| Ctrl+6, Ctrl+8, Ctrl+Q, Ctrl+Shift+A, Ctrl+A after a function name, Ctrl+Shift+U, Ctrl+Alt+P | same | objects, outline symbols, Quick Analysis, argument names, Function Arguments, formula bar height, tooltips | missing | +| Ctrl+Shift+Page Up / Down, Ctrl+Alt+5, Ctrl+Shift+F1, Ctrl+F5–F10, Alt+F8 / F11 / F12 | same | group sheets, shape cycling, full screen, window keys, macros, VBA, Power Query | missing | +| — | Cmd+= (calculate), Cmd+T (references while editing; F4 works), Shift+F7, Cmd+Shift+L | Mac-only extras | missing (Cmd+= is egui's zoom key; no thesaurus or Modify Cell Style) | + +On Windows, AltGr arrives as Ctrl+Alt: a Ctrl+Alt key that also types a character is treated as +text, so AltGr symbols (e.g. `~` on a German layout) never trigger a shortcut. ## Dialogs @@ -84,7 +136,7 @@ by rule and will never match Office's, by design. ## Work (50–80 h) -Key tips 3–5 · shortcut audit 6–10 · context menus and Quick Analysis 5–8 · Page Layout view +Key tips 3–5 · remaining shortcuts with their features 2–4 · context menus and Quick Analysis 5–8 · Page Layout view 8–12 · Options dialog and ribbon/QAT customization 6–10 · accessibility 12–18 · selection and scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. @@ -95,5 +147,6 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | 2026-10-11 | minor | Delete and Backspace now remove the selected chart, picture or shape (#213) | | 2026-10-10 | minor | Keep formula helpers inside the worksheet when scrolling (#200) | | 2026-10-11 | trivial | Theme: title-bar light/dark switch, File › Display theme, system appearance on first run (#194, #239) | +| 2026-10-11 | minor | Keyboard shortcut audit against Microsoft's Windows and Mac lists; missing shortcuts for existing features added (#110) | | 2026-10-10 | minor | Chart resize bug (#190) | | 2026-10-10 | major | Created: chrome, grid, keyboard, dialogs, accessibility; key tips, Page Layout view and accessibility tree flagged |