Keytips: letters typed with Alt held continue the sequence (fixes #1846)

Alt+H, O, A (AutoFit Row Height) and Alt+H, O, I (AutoFit Column Width)
ran nothing when Alt was still held for H, as Excel allows: any key with
Alt down cancelled keytips. KeyTips now tracks the left Alt key, and a
letter typed with it held that continues a known sequence is handled as
a keytip letter. Other Alt combinations still cancel; AltGr (right Alt)
and macOS Option are unchanged (#1864).
This commit is contained in:
Shnrqpdr
2026-10-19 00:14:35 -03:00
parent 66ce6be474
commit 4ed93c3b2c
3 changed files with 97 additions and 3 deletions
+20 -1
View File
@@ -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();
+74
View File
@@ -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 -2
View File
@@ -1,6 +1,6 @@
# UI parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (horizontal scrollbar in the sheet tab bar, #840) · **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 +77,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)
@@ -161,6 +161,7 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12.
| Date | Change | Summary |
|---|---|---|
| 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 |