Keyboard: Excel shortcut audit, add the missing ones (#110)
Audited the grid, ribbon and editor key handling against Microsoft's published Windows and Mac shortcut lists. New keymap.rs binds the ones whose feature already exists: Ctrl+Shift+~!@#$%^&_ number formats and borders, Ctrl+2-5, Ctrl+Shift+: time, Ctrl+' / Ctrl+Shift+" from the cell above, Ctrl+L, Ctrl+Shift+L, Ctrl+P, Ctrl+Alt+V, Alt+=, Alt+Down, Alt+Page Down/Up, Ctrl+Shift+Home/End, Shift+Home, the F-key family (Shift+F2/F3/F9/F10/F11, F5, F7, F12, Ctrl+F1/F3/F4, Alt+F1, Ctrl+Alt+F9) and the Excel for Mac variants (Cmd+L/E alignment, Control+E Flash Fill, Cmd+Option borders, Cmd+Shift+K/J/H/R). Number-format keys match the physical key; AltGr text never triggers a Ctrl+Alt shortcut. Fixes the command catalog's wrong shortcut labels and documents the audit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9f896c1249
commit
76c620fc32
|
|
@ -29,7 +29,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"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
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
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",
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
// 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)),
|
||||
|
|
|
|||
|
|
@ -143,8 +143,10 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
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),
|
||||
|
|
|
|||
|
|
@ -1666,8 +1666,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| {
|
||||
|
|
|
|||
|
|
@ -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<Key>, m: Modifiers, mac: bool) -> Option<Shortcut> {
|
||||
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<Key>, 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created from a source audit and the issue tracker) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (keyboard shortcut audit, #110) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
|
||||
Interaction, controls and feel, against Excel. Part of [target-app-parity.md](target-app-parity.md).
|
||||
**UI/UX fidelity: ~55% ready, 50–80 h** (estimated). Ribbon command coverage is measured
|
||||
|
|
@ -51,15 +51,62 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes
|
|||
|
||||
| Area | Excel | GridCraft |
|
||||
|---|---|---|
|
||||
| Shortcuts bound to commands | 200+ documented shortcuts per platform | 59 command shortcuts plus grid and editor keys (55 distinct keys handled in `ui-egui`) |
|
||||
| Ctrl+arrow, Ctrl+Shift+arrow, Ctrl+Home/End, Page Up/Down, Alt+Page | yes | done |
|
||||
| Ctrl+1, Ctrl+; and Ctrl+Shift+; , Ctrl+D/R, Ctrl+E, Ctrl+T, Ctrl+K, Ctrl+Shift+L | yes | most done; audit pending |
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
|
|
@ -83,7 +130,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.
|
||||
|
||||
|
|
@ -91,5 +138,6 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
|||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue