mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 02:36:48 -06:00
Merge pull request #1902 from Shnrqpdr:fix/1846-autofit-keytips
# Conflicts: # docs/ui-parity.md
This commit is contained in:
@@ -65,3 +65,6 @@ sync_github_name = true
|
||||
[people.Silvertree2010]
|
||||
display_name = "Netter"
|
||||
sync_github_name = false
|
||||
|
||||
[people.Shnrqpdr]
|
||||
sync_github_name = true
|
||||
|
||||
@@ -24,6 +24,8 @@ pub enum Action {
|
||||
pub struct KeyTips {
|
||||
prefix: Option<String>,
|
||||
alt_armed: bool,
|
||||
// Left Alt is down: Alt+H (held, as Excel allows) types keytip letters.
|
||||
alt_held: bool,
|
||||
// Keep consuming a held mnemonic after its action closes keytips.
|
||||
held: Vec<Key>,
|
||||
}
|
||||
@@ -36,6 +38,7 @@ impl KeyTips {
|
||||
pub fn clear(&mut self) {
|
||||
self.prefix = None;
|
||||
self.alt_armed = false;
|
||||
self.alt_held = false;
|
||||
self.held.clear();
|
||||
}
|
||||
|
||||
@@ -61,6 +64,9 @@ impl KeyTips {
|
||||
Event::PointerButton { pressed: true, .. } | Event::MouseWheel { .. } | Event::WindowFocused(false) | Event::Ime(_) => self.clear(),
|
||||
Event::Copy | Event::Cut | Event::Paste(_) => self.clear(),
|
||||
Event::ModifiersChanged(m) => {
|
||||
if !m.alt {
|
||||
self.alt_held = false;
|
||||
}
|
||||
if command_modifier(*m) || (mac && m.alt) {
|
||||
self.clear();
|
||||
} else if m.shift {
|
||||
@@ -68,6 +74,7 @@ impl KeyTips {
|
||||
}
|
||||
}
|
||||
Event::Key { key: Key::AltLeft, pressed, repeat, modifiers, .. } if !mac && !command_modifier(*modifiers) && !modifiers.shift => {
|
||||
self.alt_held = *pressed;
|
||||
if *pressed {
|
||||
if !repeat {
|
||||
self.alt_armed = true;
|
||||
@@ -80,8 +87,20 @@ impl KeyTips {
|
||||
}
|
||||
Event::Key { key, pressed, repeat, modifiers, .. } => {
|
||||
self.alt_armed = false;
|
||||
// A letter typed with left Alt held continues a sequence (Alt+H, O, A), as
|
||||
// in Excel; any other Alt combination cancels keytips.
|
||||
let alt_letter =
|
||||
self.alt_held && modifiers.alt && !modifiers.shift && !command_modifier(*modifiers) && key.name().len() == 1 && {
|
||||
let next = format!("{}{}", self.prefix.as_deref().unwrap_or_default(), key.name());
|
||||
keytip_table::lookup(&next).is_some() || keytip_table::has_deeper(&next)
|
||||
};
|
||||
if alt_letter && *pressed && self.prefix.is_none() {
|
||||
self.prefix = Some(String::new());
|
||||
}
|
||||
// The release of that letter still carries Alt.
|
||||
let alt_release = !*pressed && self.held.contains(key);
|
||||
if command_modifier(*modifiers)
|
||||
|| modifiers.alt
|
||||
|| (modifiers.alt && !alt_letter && !alt_release)
|
||||
|| matches!(key, Key::AltLeft | Key::AltRight | Key::ControlLeft | Key::ControlRight | Key::SuperLeft | Key::SuperRight)
|
||||
{
|
||||
self.clear();
|
||||
|
||||
@@ -267,6 +267,80 @@ fn mouse_press_and_window_focus_loss_cancel_keytips() {
|
||||
assert!(h.state().editor.is_none());
|
||||
}
|
||||
|
||||
fn alt_down(h: &mut Harness, alt: Key) {
|
||||
events(h, [Event::ModifiersChanged(Modifiers::ALT), key_event(alt, true, Modifiers::ALT)]);
|
||||
}
|
||||
|
||||
fn alt_up(h: &mut Harness, alt: Key) {
|
||||
events(h, [Event::ModifiersChanged(Modifiers::NONE), key_event(alt, false, Modifiers::NONE)]);
|
||||
}
|
||||
|
||||
fn alt_letter(h: &mut Harness, key: Key) {
|
||||
events(h, [key_event(key, true, Modifiers::ALT), key_event(key, false, Modifiers::ALT)]);
|
||||
}
|
||||
|
||||
fn line_size(h: &Harness, cols: bool) -> Option<f32> {
|
||||
let sh = h.state().session.active().unwrap().wb.active().unwrap();
|
||||
if cols { &sh.cols } else { &sh.rows }.get(&0).and_then(|e| e.size)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn letters_typed_with_alt_held_run_autofit_sequences() {
|
||||
// #1846: Excel accepts Alt+H, O, A with Alt still held for the first letter (or all of them).
|
||||
for os in [OperatingSystem::Windows, OperatingSystem::Nix] {
|
||||
let mut s = blank();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "a much longer piece of text than the column"})).unwrap();
|
||||
s.execute("home.fontSize", json!({"range": "A1", "size": 28})).unwrap();
|
||||
s.execute("home.rowHeight", json!({"rows": "1:1", "height": 10})).unwrap();
|
||||
s.execute("selection.set", json!({"cell": "A1"})).unwrap();
|
||||
let mut h = harness_on(s, os);
|
||||
|
||||
// Alt held through H, O and I: AutoFit Column Width.
|
||||
alt_down(&mut h, Key::AltLeft);
|
||||
alt_letter(&mut h, Key::H);
|
||||
assert_eq!(h.state().keytips.prefix(), Some("H"), "{os:?}: Alt+H opens the Home keytips");
|
||||
alt_letter(&mut h, Key::O);
|
||||
alt_letter(&mut h, Key::I);
|
||||
alt_up(&mut h, Key::AltLeft);
|
||||
assert_eq!(h.state().keytips.prefix(), None, "{os:?}");
|
||||
assert!(line_size(&h, true).is_some_and(|w| w > 100.0), "{os:?}: column A was autofitted");
|
||||
|
||||
// Alt released after H: AutoFit Row Height.
|
||||
alt_down(&mut h, Key::AltLeft);
|
||||
alt_letter(&mut h, Key::H);
|
||||
alt_up(&mut h, Key::AltLeft);
|
||||
assert_eq!(h.state().keytips.prefix(), Some("H"), "{os:?}: releasing Alt keeps the level");
|
||||
letter(&mut h, Key::O, false);
|
||||
letter(&mut h, Key::A, false);
|
||||
assert!(line_size(&h, false).is_some_and(|r| r > 10.0), "{os:?}: row 1 was autofitted");
|
||||
assert_eq!(value(&h, "A1"), Value::Text("a much longer piece of text than the column".into()));
|
||||
assert!(h.state().editor.is_none(), "{os:?}: no letter reached the grid");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_combinations_outside_a_sequence_still_cancel_keytips() {
|
||||
let mut h = harness_on(blank(), OperatingSystem::Windows);
|
||||
alt_down(&mut h, Key::AltLeft);
|
||||
alt_letter(&mut h, Key::X);
|
||||
alt_up(&mut h, Key::AltLeft);
|
||||
assert_eq!(h.state().keytips.prefix(), None, "X starts no sequence");
|
||||
|
||||
// AltGr types characters (and is Ctrl+Alt on Windows): it never starts keytips.
|
||||
let mut h = harness_on(blank(), OperatingSystem::Nix);
|
||||
alt_down(&mut h, Key::AltRight);
|
||||
alt_letter(&mut h, Key::H);
|
||||
alt_up(&mut h, Key::AltRight);
|
||||
assert_eq!(h.state().keytips.prefix(), None);
|
||||
|
||||
// macOS: Option is a text modifier (see #1864).
|
||||
let mut h = harness_on(blank(), OperatingSystem::Mac);
|
||||
alt_down(&mut h, Key::AltLeft);
|
||||
alt_letter(&mut h, Key::H);
|
||||
alt_up(&mut h, Key::AltLeft);
|
||||
assert_eq!(h.state().keytips.prefix(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn held_keys_do_not_toggle_twice_or_leak_repeated_letters() {
|
||||
let mut h = harness(blank());
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
# UI parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (Ctrl+Enter fill gives each cell the automatic number format typing gives it) · **Target:** Microsoft Excel (Microsoft 365), Excel for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (keytip letters typed with Alt held, #1846) · **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
|
||||
@@ -77,7 +78,7 @@ this logic, with one undo step.
|
||||
| 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) |
|
||||
| Sequential Alt access keys (Alt, H, N, …) on every ribbon tab | yes | 364-sequence keytip table for all tabs: per-control badges, prefix waiting, dropdowns open for editing (type/arrows/Enter), pending bindings for not-yet-built commands; QAT/Backstage excluded |
|
||||
| Sequential Alt access keys (Alt, H, N, …) on every ribbon tab | yes | 364-sequence keytip table for all tabs: per-control badges, prefix waiting, dropdowns open for editing (type/arrows/Enter), pending bindings for not-yet-built commands; letters also work with left Alt held (Alt+H, O, A), as in Excel; QAT/Backstage excluded |
|
||||
| Requests | — | #110 (more shortcuts and hot keys), #33 (sequential Alt; initial support landed in #43) |
|
||||
|
||||
### Shortcut audit (#110)
|
||||
@@ -162,6 +163,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-19 | minor | Ctrl+Enter (`range.fill`) gives each cell what typing the input there gives: a General cell gets the automatic number format (`5%`, `$5`, `3/4/2026`, `=TODAY()`) even when it already held a value, and a cell with its own format, a formatted row or column, or Text keeps it. Cells that held a value used to keep General (`5%` showed 0.05), and empty cells took the active cell's format |
|
||||
| 2026-10-18 | minor | Keytip letters typed with left Alt held continue the sequence (Alt+H, O, A / Alt+H, O, I ran nothing before: Alt+letter cancelled keytips); other Alt combinations, AltGr and macOS Option are unchanged (#1846) |
|
||||
| 2026-10-12 | minor | The File view's recent list tells when each workbook was last changed, read off the UI thread |
|
||||
| 2026-10-13 | minor | Horizontal scrollbar in the sheet tab bar: drag, track click pages, frozen columns excluded from the page (#840) |
|
||||
| 2026-10-12 | minor | Ctrl+wheel zoom steps along a ladder of percentages about 20% apart that never skips 100%, gliding to each step with animations on, so a notch out undoes a notch in (it drifted: 100 → 122 → 101); Excel steps 15 points, see decisions.md |
|
||||
|
||||
Reference in New Issue
Block a user