From b415d2e83562fd7a23c699b3e32d8af263494865 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 19:05:19 +0300 Subject: [PATCH 01/15] Keytips: table-driven matcher and badge painting (issues #33, #49) Move the hardcoded keytip sequence list into keytip_table.rs as the single source of truth for both the matcher and the ribbon badge painter. - Generalize Action::Home into Action::Tab(&'static str); the legacy Edit/Format prefix special-case in lib.rs is absorbed into the table. - Badge helpers: tab_sequence (tabs), group_extension (menu anchors), item_letter (menu items), control_badges (command controls). - Add tab letters for Insert (N), Page Layout (P), Formulas (M), Data (A), Review (R), View (W); Draw/Automate stay unbadged until contextual letters land. - Tests: 5 table unit tests + tab_letters_switch_to_their_ribbon_tabs integration test. All 7 xtask ci gates green. --- crates/ui-egui/src/keytips.rs | 27 ++-- crates/ui-egui/src/keytips/keytip_table.rs | 165 +++++++++++++++++++++ crates/ui-egui/src/lib.rs | 11 +- crates/ui-egui/src/ribbon.rs | 55 ++++--- crates/ui-egui/tests/keytips.rs | 13 ++ 5 files changed, 226 insertions(+), 45 deletions(-) create mode 100644 crates/ui-egui/src/keytips/keytip_table.rs diff --git a/crates/ui-egui/src/keytips.rs b/crates/ui-egui/src/keytips.rs index a5bf155..d75a544 100644 --- a/crates/ui-egui/src/keytips.rs +++ b/crates/ui-egui/src/keytips.rs @@ -1,10 +1,14 @@ //! Sequential ribbon shortcuts. The host paints hints from `prefix` and runs returned actions. +//! +//! The sequence table lives in [`keytip_table`]: the match below only walks the prefix. use egui::{Context, Event, Key, Modifiers, os::OperatingSystem}; +pub mod keytip_table; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Action { - Home, + Tab(&'static str), Command(&'static str), Dialog(&'static str), } @@ -92,21 +96,16 @@ impl KeyTips { } } else if key.name().len() == 1 { let next = format!("{}{key_name}", self.prefix.as_deref().unwrap_or_default(), key_name = key.name()); - match next.as_str() { - "H" => { + match keytip_table::lookup(&next) { + Some(keytip_table::Entry::Tab(tab)) => { self.prefix = Some(next); - actions.push(Action::Home); + actions.push(Action::Tab(tab)); } - "HA" | "HV" | "HO" | "E" | "O" | "OC" => self.prefix = Some(next), - "HAR" => self.finish(Action::Command("home.alignRight"), &mut actions), - "HAL" => self.finish(Action::Command("home.alignLeft"), &mut actions), - "HAC" => self.finish(Action::Command("home.alignCenter"), &mut actions), - "HAT" => self.finish(Action::Command("home.alignTop"), &mut actions), - "HAM" => self.finish(Action::Command("home.alignMiddle"), &mut actions), - "HAB" => self.finish(Action::Command("home.alignBottom"), &mut actions), - "HVS" | "ES" => self.finish(Action::Dialog("pasteSpecial"), &mut actions), - "HOI" | "OCA" => self.finish(Action::Command("home.autofitColumnWidth"), &mut actions), - _ => {} + Some(keytip_table::Entry::Continue) => self.prefix = Some(next), + Some(keytip_table::Entry::Finish(action)) => self.finish(action, &mut actions), + // Unknown letters are consumed while a prefix is active but + // leave it unchanged, as Excel does. + None => {} } } } diff --git a/crates/ui-egui/src/keytips/keytip_table.rs b/crates/ui-egui/src/keytips/keytip_table.rs new file mode 100644 index 0000000..9503647 --- /dev/null +++ b/crates/ui-egui/src/keytips/keytip_table.rs @@ -0,0 +1,165 @@ +//! The keytip sequence table: every Excel-style Alt access sequence and what it does. +//! +//! This is the single source of truth for sequential access keys. Both the matcher in +//! [`super::KeyTips::process`] and the badge painter in `ribbon.rs` read from here, so a +//! sequence is added in exactly one place. +//! +//! Regenerated (T5) from `plan/keytips-map-results.json` — the Excel-sequence collection and +//! catalog join. Do not hand-edit entries without updating that source data. + +use super::Action; + +/// What a fully-matched sequence does. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Entry { + /// Switch to this ribbon tab and keep the prefix: the tab's own badges take over. + Tab(&'static str), + /// Keep the prefix: this sequence is a group-level badge; deeper sequences finish it. + Continue, + /// Complete the sequence: run the action and clear the prefix. + Finish(Action), +} + +/// All known sequences, in no particular order (lookup is linear; sizes are small). +pub const SEQUENCES: &[(&str, Entry)] = &[ + // Ribbon tabs. Legacy Edit/Format paths (E/O/OC) lead to the same Home menus. + ("H", Entry::Tab("Home")), + ("N", Entry::Tab("Insert")), + ("P", Entry::Tab("Page Layout")), + ("M", Entry::Tab("Formulas")), + ("A", Entry::Tab("Data")), + ("R", Entry::Tab("Review")), + ("W", Entry::Tab("View")), + ("E", Entry::Tab("Home")), + ("O", Entry::Tab("Home")), + // Home tab groups. + ("HA", Entry::Continue), + ("HV", Entry::Continue), + ("HO", Entry::Continue), + ("OC", Entry::Continue), + // Home > Alignment. + ("HAR", Entry::Finish(Action::Command("home.alignRight"))), + ("HAL", Entry::Finish(Action::Command("home.alignLeft"))), + ("HAC", Entry::Finish(Action::Command("home.alignCenter"))), + // Paste Special — ribbon and legacy routes. + ("HVS", Entry::Finish(Action::Dialog("pasteSpecial"))), + ("ES", Entry::Finish(Action::Dialog("pasteSpecial"))), + // AutoFit column width — ribbon and legacy routes. + ("HOI", Entry::Finish(Action::Command("home.autofitColumnWidth"))), + ("OCA", Entry::Finish(Action::Command("home.autofitColumnWidth"))), +]; + +/// Look up a complete sequence. +pub fn lookup(sequence: &str) -> Option { + SEQUENCES.iter().find(|(seq, _)| *seq == sequence).map(|(_, entry)| *entry) +} + +/// True if `prefix` is a strict prefix of any known sequence (badges wait for more keys). +pub fn has_deeper(prefix: &str) -> bool { + SEQUENCES.iter().any(|(seq, _)| seq.len() > prefix.len() && seq.starts_with(prefix)) +} + +/// The one-letter sequence that switches to `tab_name`, if that tab has one. +pub fn tab_sequence(tab_name: &str) -> Option<&'static str> { + SEQUENCES.iter().find_map(|(seq, entry)| match entry { + Entry::Tab(name) if *name == tab_name && seq.len() == 1 => Some(*seq), + _ => None, + }) +} + +fn matches_target(action: &Action, target: &str) -> bool { + match action { + Action::Command(id) => *id == target, + Action::Dialog(name) => format!("dialog:{name}") == target, + Action::Tab(_) => false, + } +} + +/// All sequences that finish by running `target` (a command id or `dialog:`). +pub fn sequences_for(target: &str) -> Vec<&'static str> { + SEQUENCES + .iter() + .filter_map(|(seq, entry)| match entry { + Entry::Finish(action) if matches_target(action, target) => Some(*seq), + _ => None, + }) + .collect() +} + +/// Group-level letters for a menu anchor: sequences one key past `prefix` that continue toward +/// any of `targets` (the menu's items) — the badge shown on the button that opens the menu. +pub fn group_extension(prefix: &str, targets: &[&str]) -> Vec<&'static str> { + let mut out = Vec::new(); + for target in targets { + for seq in sequences_for(target) { + if seq.len() <= prefix.len() + 1 || !seq.starts_with(prefix) { + continue; + } + // The intermediate one key past the prefix is the group badge, if it is a + // group-level (Continue) sequence rather than the finish itself. + let candidate = &seq[..prefix.len() + 1]; + if matches!(lookup(candidate), Some(Entry::Continue)) && !out.contains(&candidate) { + out.push(candidate); + } + } + } + out +} + +/// The badge for a menu item: its finish sequence exactly one key below the current prefix. +pub fn item_letter(prefix: &str, target: &str) -> Option<&'static str> { + sequences_for(target).into_iter().find(|seq| seq.len() == prefix.len() + 1 && seq.starts_with(prefix)) +} + +/// Badge sequences for a command control: every sequence for `target` that continues from the +/// current prefix (the painter strips the prefix, so the remaining letters show). +pub fn control_badges(prefix: &str, target: &str) -> Vec<&'static str> { + if lookup(prefix).is_none() { + return Vec::new(); + } + sequences_for(target).into_iter().filter(|seq| seq.len() > prefix.len() && seq.starts_with(prefix)).collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tab_letters_exist_for_standard_tabs() { + for (tab, letter) in [("Home", "H"), ("Insert", "N"), ("Page Layout", "P"), ("Formulas", "M"), ("Data", "A"), ("Review", "R"), ("View", "W")] + { + assert_eq!(tab_sequence(tab), Some(letter), "tab {tab}"); + } + assert_eq!(tab_sequence("Draw"), None); + assert_eq!(tab_sequence("Automate"), None); + } + + #[test] + fn sequences_for_finds_command_and_dialog_targets() { + assert_eq!(sequences_for("home.alignRight"), vec!["HAR"]); + assert_eq!(sequences_for("dialog:pasteSpecial"), vec!["HVS", "ES"]); + assert_eq!(sequences_for("home.autofitColumnWidth"), vec!["HOI", "OCA"]); + } + + #[test] + fn group_extension_paints_only_continue_levels() { + assert_eq!(group_extension("H", &["dialog:pasteSpecial"]), vec!["HV"]); + assert!(group_extension("", &["dialog:pasteSpecial"]).is_empty(), "tab letters paint on tabs, not buttons"); + assert!(group_extension("E", &["dialog:pasteSpecial"]).is_empty(), "the finish letter paints on the menu item"); + } + + #[test] + fn item_letter_is_one_level_deep() { + assert_eq!(item_letter("HV", "dialog:pasteSpecial"), Some("HVS")); + assert_eq!(item_letter("E", "dialog:pasteSpecial"), Some("ES")); + assert_eq!(item_letter("H", "dialog:pasteSpecial"), None, "the item badge waits for its group letter"); + assert_eq!(item_letter("HO", "home.autofitColumnWidth"), Some("HOI")); + } + + #[test] + fn control_badges_require_a_known_prefix() { + assert!(control_badges("", "home.alignRight").is_empty(), "empty prefix shows only tab badges"); + assert_eq!(control_badges("HA", "home.alignRight"), vec!["HAR"]); + assert!(control_badges("HZ", "home.alignRight").is_empty(), "unknown prefixes show nothing"); + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9ef0d50..9c240e3 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1007,17 +1007,10 @@ impl SheetApp { // Leave pointer cancellation to the popup so clicking a menu item still works. egui::Popup::close_all(ctx); } - // The legacy Edit/Format paths lead to the same existing Home menus. Switch once, when - // the prefix changes, not every frame (that would reset a tab the user picked meanwhile). - let after = self.keytips.prefix(); - if after != before.as_deref() && matches!(after, Some("E" | "O" | "OC")) { - self.run_or_alert("ui.ribbonTab", json!({"tab": "Home"})); - self.ui.ribbon_collapsed = false; - } for action in actions { match action { - keytips::Action::Home => { - self.run_or_alert("ui.ribbonTab", json!({"tab": "Home"})); + keytips::Action::Tab(tab) => { + self.run_or_alert("ui.ribbonTab", json!({"tab": tab})); self.ui.ribbon_collapsed = false; } keytips::Action::Command(id) => self.run_or_alert(id, json!({})), diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index c2728a3..dff93f0 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -6,6 +6,7 @@ use serde_json::json; use crate::SheetApp; use crate::icons::{self, Icon}; +use crate::keytips::keytip_table; use crate::theme::{self, Tokens}; use crate::widgets::{big_button, color_palette, icon_button, small_button, split_button, toggle_button}; @@ -203,8 +204,10 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { if resp.double_clicked() { app.ui.ribbon_collapsed = !collapsed; } - if tab == "Home" && app.keytips.prefix() == Some("") { - keytip(app, ui, "H", pos2(r.center().x, r.bottom() - 1.0)); + if app.keytips.prefix() == Some("") + && let Some(sequence) = keytip_table::tab_sequence(tab) + { + keytip(app, ui, sequence, pos2(r.center().x, r.bottom() - 1.0)); } } }); @@ -328,14 +331,7 @@ fn menu_items(app: &mut SheetApp, ui: &mut Ui, items: &[(&str, &str, serde_json: } let lang = app.ui.language; let response = ui.add(egui::Button::new(lang.tr(label)).frame(false).min_size(vec2(220.0, 22.0))); - let sequence = match (app.keytips.prefix(), *id) { - (Some("HV"), "dialog:pasteSpecial") => Some("HVS"), - (Some("E"), "dialog:pasteSpecial") => Some("ES"), - (Some("HO"), "home.autofitColumnWidth") => Some("HOI"), - (Some("OC"), "home.autofitColumnWidth") => Some("OCA"), - _ => None, - }; - if let Some(sequence) = sequence { + if let Some(sequence) = keytip_table::item_letter(app.keytips.prefix().unwrap_or_default(), id) { keytip(app, ui, sequence, pos2(response.rect.right() - 10.0, response.rect.center().y)); } if response.clicked() { @@ -353,8 +349,10 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { let st = active_style(app); // Clipboard let paste = big_button(ui, Icon::Paste, "Paste", "Paste (⌘V)", true); - if app.keytips.prefix() == Some("H") { - keytip(app, ui, "HV", paste.rect.center_bottom()); + for sequence in + keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["edit.paste", "edit.pasteSpecial", "dialog:pasteSpecial"]) + { + keytip(app, ui, sequence, paste.rect.center_bottom()); } if paste.clicked() { if let Some(text) = app.grid.system_clipboard.take() { @@ -576,12 +574,7 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { (Icon::AlignRight, HAlign::Right, "home.alignRight", "Align Right"), ] { let response = toggle_button(ui, icon, st.align.h == h, tip); - if matches!(app.keytips.prefix(), Some("H" | "HA")) { - let sequence = match h { - HAlign::Left => "HAL", - HAlign::Center => "HAC", - _ => "HAR", - }; + for sequence in keytip_table::control_badges(app.keytips.prefix().unwrap_or_default(), id) { keytip(app, ui, sequence, response.rect.center_bottom()); } if response.clicked() { @@ -700,10 +693,28 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ); }); let fmt = small_button(ui, Icon::Format, "Format", "Format", true); - match app.keytips.prefix() { - Some("H") => keytip(app, ui, "HO", fmt.rect.center_bottom()), - Some("O") => keytip(app, ui, "OC", fmt.rect.center_bottom()), - _ => {} + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &[ + "dialog:rowHeight", + "home.autofitRowHeight", + "dialog:columnWidth", + "home.autofitColumnWidth", + "dialog:defaultWidth", + "home.hideRows", + "home.hideColumns", + "home.unhideRows", + "home.unhideColumns", + "sheet.hide", + "sheet.unhide", + "dialog:renameSheet", + "dialog:moveSheet", + "dialog:protectSheet", + "home.lockCell", + "dialog:formatCells", + ], + ) { + keytip(app, ui, sequence, fmt.rect.center_bottom()); } let format_keys = matches!(app.keytips.prefix(), Some("HO" | "OC")); let mut format_popup = egui::Popup::menu(&fmt); diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index 272f863..02a02b1 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -295,6 +295,19 @@ fn held_keys_do_not_toggle_twice_or_leak_repeated_letters() { assert_eq!(value(&h, "A1"), Value::Empty); } +#[test] +fn tab_letters_switch_to_their_ribbon_tabs() { + let mut h = harness(blank()); + for (k, tab) in [(Key::N, "Insert"), (Key::P, "Page Layout"), (Key::M, "Formulas"), (Key::A, "Data"), (Key::R, "Review"), (Key::W, "View")] { + key(&mut h, Key::F10); // arm + letter(&mut h, k, false); + assert_eq!(h.state().ui.ribbon_tab, tab, "letter {k:?}"); + assert!(h.state().editor.is_none(), "tab letters must not begin a cell edit"); + key(&mut h, Key::F10); // disarm + assert_eq!(h.state().keytips.prefix(), None); + } +} + #[test] fn ordinary_typing_editor_shortcuts_and_altgr_remain_text_input() { let mut h = harness(blank()); From eba3957a210682bddafbab5041f82051f4fd4879 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 19:30:21 +0300 Subject: [PATCH 02/15] Keytips: multi-letter units and implicit wait levels (Draw = "JI") Excel shows some access keys as one multi-letter unit (Draw's "JI") that fires only once complete. The matcher now follows the trie shape: - A pressed letter that matches a complete sequence fires immediately. - If it only starts deeper sequences, the matcher waits for more input (implicit group level, no table entry required). - A tab with deeper sequences stays as an active level so group badges show; a terminal unit clears the prefix. - tab_sequence() returns multi-letter tab units as one badge. Draw: Alt+JI switches to the Draw tab; deeper Draw letters are left blank for future contributors (GridCraft's Draw differs from Excel's). --- crates/ui-egui/src/keytips.rs | 8 +++- crates/ui-egui/src/keytips/keytip_table.rs | 45 +++++++++++++++++----- crates/ui-egui/tests/keytips.rs | 35 ++++++++++++++++- 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/keytips.rs b/crates/ui-egui/src/keytips.rs index d75a544..0d45980 100644 --- a/crates/ui-egui/src/keytips.rs +++ b/crates/ui-egui/src/keytips.rs @@ -98,11 +98,17 @@ impl KeyTips { let next = format!("{}{key_name}", self.prefix.as_deref().unwrap_or_default(), key_name = key.name()); match keytip_table::lookup(&next) { Some(keytip_table::Entry::Tab(tab)) => { - self.prefix = Some(next); + // A tab with deeper sequences stays as an active level + // (its group badges show); a terminal unit like "JI" + // finishes and clears the prefix. + self.prefix = keytip_table::has_deeper(&next).then_some(next); actions.push(Action::Tab(tab)); } Some(keytip_table::Entry::Continue) => self.prefix = Some(next), Some(keytip_table::Entry::Finish(action)) => self.finish(action, &mut actions), + // No complete sequence, but deeper ones start here: wait for + // more input (Excel's two-letter units like Draw's "JI"). + None if keytip_table::has_deeper(&next) => self.prefix = Some(next), // Unknown letters are consumed while a prefix is active but // leave it unchanged, as Excel does. None => {} diff --git a/crates/ui-egui/src/keytips/keytip_table.rs b/crates/ui-egui/src/keytips/keytip_table.rs index 9503647..df42f12 100644 --- a/crates/ui-egui/src/keytips/keytip_table.rs +++ b/crates/ui-egui/src/keytips/keytip_table.rs @@ -30,6 +30,9 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("A", Entry::Tab("Data")), ("R", Entry::Tab("Review")), ("W", Entry::Tab("View")), + // Draw is a two-letter unit in Excel: the badge shows "JI" as one keytip, and the + // matcher waits after "J" (implicit level — no table entry needed). + ("JI", Entry::Tab("Draw")), ("E", Entry::Tab("Home")), ("O", Entry::Tab("Home")), // Home tab groups. @@ -59,14 +62,21 @@ pub fn has_deeper(prefix: &str) -> bool { SEQUENCES.iter().any(|(seq, _)| seq.len() > prefix.len() && seq.starts_with(prefix)) } -/// The one-letter sequence that switches to `tab_name`, if that tab has one. +/// The sequence that switches to `tab_name`, if that tab has one (may be a multi-letter unit +/// like Draw's "JI"). pub fn tab_sequence(tab_name: &str) -> Option<&'static str> { SEQUENCES.iter().find_map(|(seq, entry)| match entry { - Entry::Tab(name) if *name == tab_name && seq.len() == 1 => Some(*seq), + Entry::Tab(name) if *name == tab_name => Some(*seq), _ => None, }) } +/// True if `seq` is a group level: an explicit Continue entry, or an implicit level that only +/// exists because deeper sequences start with it (like "J" before Draw's "JI"). +pub fn is_group_level(seq: &str) -> bool { + matches!(lookup(seq), Some(Entry::Continue)) || (lookup(seq).is_none() && has_deeper(seq)) +} + fn matches_target(action: &Action, target: &str) -> bool { match action { Action::Command(id) => *id == target, @@ -96,9 +106,9 @@ pub fn group_extension(prefix: &str, targets: &[&str]) -> Vec<&'static str> { continue; } // The intermediate one key past the prefix is the group badge, if it is a - // group-level (Continue) sequence rather than the finish itself. + // group-level sequence (explicit or implicit) rather than the finish itself. let candidate = &seq[..prefix.len() + 1]; - if matches!(lookup(candidate), Some(Entry::Continue)) && !out.contains(&candidate) { + if is_group_level(candidate) && !out.contains(&candidate) { out.push(candidate); } } @@ -114,8 +124,8 @@ pub fn item_letter(prefix: &str, target: &str) -> Option<&'static str> { /// Badge sequences for a command control: every sequence for `target` that continues from the /// current prefix (the painter strips the prefix, so the remaining letters show). pub fn control_badges(prefix: &str, target: &str) -> Vec<&'static str> { - if lookup(prefix).is_none() { - return Vec::new(); + if prefix.is_empty() { + return Vec::new(); // empty prefix shows only tab badges } sequences_for(target).into_iter().filter(|seq| seq.len() > prefix.len() && seq.starts_with(prefix)).collect() } @@ -126,11 +136,18 @@ mod tests { #[test] fn tab_letters_exist_for_standard_tabs() { - for (tab, letter) in [("Home", "H"), ("Insert", "N"), ("Page Layout", "P"), ("Formulas", "M"), ("Data", "A"), ("Review", "R"), ("View", "W")] - { + for (tab, letter) in [ + ("Home", "H"), + ("Insert", "N"), + ("Page Layout", "P"), + ("Formulas", "M"), + ("Data", "A"), + ("Review", "R"), + ("View", "W"), + ("Draw", "JI"), // two-letter unit, shown as one badge + ] { assert_eq!(tab_sequence(tab), Some(letter), "tab {tab}"); } - assert_eq!(tab_sequence("Draw"), None); assert_eq!(tab_sequence("Automate"), None); } @@ -141,6 +158,16 @@ mod tests { assert_eq!(sequences_for("home.autofitColumnWidth"), vec!["HOI", "OCA"]); } + #[test] + fn implicit_levels_wait_for_deeper_sequences() { + assert!(has_deeper("J"), "JI is reachable only through the implicit J level"); + assert!(is_group_level("J"), "implicit levels are group levels"); + assert!(is_group_level("HV"), "explicit Continue levels are group levels"); + assert!(!is_group_level("H"), "a terminal tab is not a group level"); + assert!(!is_group_level("HVS"), "a finish is not a group level"); + assert!(!has_deeper("X")); + } + #[test] fn group_extension_paints_only_continue_levels() { assert_eq!(group_extension("H", &["dialog:pasteSpecial"]), vec!["HV"]); diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index 02a02b1..c684ddb 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -302,12 +302,43 @@ fn tab_letters_switch_to_their_ribbon_tabs() { key(&mut h, Key::F10); // arm letter(&mut h, k, false); assert_eq!(h.state().ui.ribbon_tab, tab, "letter {k:?}"); + assert_eq!(h.state().keytips.prefix(), None, "terminal tab units finish cleanly"); assert!(h.state().editor.is_none(), "tab letters must not begin a cell edit"); - key(&mut h, Key::F10); // disarm - assert_eq!(h.state().keytips.prefix(), None); } } +#[test] +fn two_letter_unit_waits_after_first_letter_then_fires() { + let mut h = harness(blank()); + key(&mut h, Key::F10); + letter(&mut h, Key::J, false); + // "J" is not a complete sequence, but "JI" starts with it: the matcher waits. + assert_eq!(h.state().keytips.prefix(), Some("J")); + assert_eq!(h.state().ui.ribbon_tab, "Home"); + letter(&mut h, Key::I, false); + assert_eq!(h.state().ui.ribbon_tab, "Draw"); + assert_eq!(h.state().keytips.prefix(), None); + assert!(h.state().editor.is_none(), "unit letters must not begin a cell edit"); +} + +#[test] +fn two_letter_unit_fires_when_pressed_in_one_frame() { + let mut h = harness(blank()); + key(&mut h, Key::F10); + events( + &mut h, + [ + key_event(Key::J, true, Modifiers::NONE), + Event::Text("j".into()), + key_event(Key::I, true, Modifiers::NONE), + Event::Text("i".into()), + key_event(Key::J, false, Modifiers::NONE), + key_event(Key::I, false, Modifiers::NONE), + ], + ); + assert_eq!(h.state().ui.ribbon_tab, "Draw"); +} + #[test] fn ordinary_typing_editor_shortcuts_and_altgr_remain_text_input() { let mut h = harness(blank()); From e90ebbbbe28c9fc15fa40e5d0d90b48b922979ad Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 19:36:14 +0300 Subject: [PATCH 03/15] Keytips: keytips.json source + xtask generator (issues #33, #49) The sequence table is no longer hand-maintained Rust: keytips.json is the single human-edited source (one line per sequence, grouped by tab, pending reserves sequences for future commands), and `cargo xtask keytips` regenerates keytip_table/generated.rs deterministically. `cargo xtask ci` now fails if the generated table is stale. - Action::Pending: reserved sequences show their badge and invoke silently until the command exists (bind-all policy). - Intermediate group levels are implicit: the matcher waits whenever a pressed letter starts a deeper sequence; only terminal entries are listed (two-letter units like Draw's "JI" work with no extra rows). - First populated sequence beyond the legacy set: W,V,G toggles gridlines, proven end-to-end by view_gridlines_sequence_runs_end_to_end. --- crates/ui-egui/src/keytips.rs | 3 + crates/ui-egui/src/keytips/keytip_table.rs | 49 ++------ .../src/keytips/keytip_table/generated.rs | 27 +++++ crates/ui-egui/src/keytips/keytips.json | 35 ++++++ crates/ui-egui/src/lib.rs | 3 + crates/ui-egui/tests/keytips.rs | 24 +++- xtask/src/keytips.rs | 111 ++++++++++++++++++ xtask/src/main.rs | 5 + 8 files changed, 217 insertions(+), 40 deletions(-) create mode 100644 crates/ui-egui/src/keytips/keytip_table/generated.rs create mode 100644 crates/ui-egui/src/keytips/keytips.json create mode 100644 xtask/src/keytips.rs diff --git a/crates/ui-egui/src/keytips.rs b/crates/ui-egui/src/keytips.rs index 0d45980..85bb214 100644 --- a/crates/ui-egui/src/keytips.rs +++ b/crates/ui-egui/src/keytips.rs @@ -11,6 +11,9 @@ pub enum Action { Tab(&'static str), Command(&'static str), Dialog(&'static str), + /// The target command does not exist yet: the sequence is reserved, its badge shows, and + /// invoking it is a silent no-op until the command lands (see keytips.json `pending`). + Pending(&'static str), } #[derive(Default)] diff --git a/crates/ui-egui/src/keytips/keytip_table.rs b/crates/ui-egui/src/keytips/keytip_table.rs index df42f12..638b6dd 100644 --- a/crates/ui-egui/src/keytips/keytip_table.rs +++ b/crates/ui-egui/src/keytips/keytip_table.rs @@ -1,13 +1,16 @@ //! The keytip sequence table: every Excel-style Alt access sequence and what it does. //! -//! This is the single source of truth for sequential access keys. Both the matcher in -//! [`super::KeyTips::process`] and the badge painter in `ribbon.rs` read from here, so a -//! sequence is added in exactly one place. +//! The matcher in [`super::KeyTips::process`] and the badge painter in `ribbon.rs` both read +//! [`SEQUENCES`], so a sequence is added in exactly one place: `keytips.json`. //! -//! Regenerated (T5) from `plan/keytips-map-results.json` — the Excel-sequence collection and -//! catalog join. Do not hand-edit entries without updating that source data. +//! `keytips.json` is the human-edited source of truth (grouped, commented, one line per +//! sequence; `pending` reserves sequences for commands that do not exist yet). `generated.rs` +//! is produced from it by `cargo xtask keytips` — edit the JSON, not the generated file. -use super::Action; +pub(crate) use super::Action; + +mod generated; +pub use generated::SEQUENCES; /// What a fully-matched sequence does. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -20,38 +23,6 @@ pub enum Entry { Finish(Action), } -/// All known sequences, in no particular order (lookup is linear; sizes are small). -pub const SEQUENCES: &[(&str, Entry)] = &[ - // Ribbon tabs. Legacy Edit/Format paths (E/O/OC) lead to the same Home menus. - ("H", Entry::Tab("Home")), - ("N", Entry::Tab("Insert")), - ("P", Entry::Tab("Page Layout")), - ("M", Entry::Tab("Formulas")), - ("A", Entry::Tab("Data")), - ("R", Entry::Tab("Review")), - ("W", Entry::Tab("View")), - // Draw is a two-letter unit in Excel: the badge shows "JI" as one keytip, and the - // matcher waits after "J" (implicit level — no table entry needed). - ("JI", Entry::Tab("Draw")), - ("E", Entry::Tab("Home")), - ("O", Entry::Tab("Home")), - // Home tab groups. - ("HA", Entry::Continue), - ("HV", Entry::Continue), - ("HO", Entry::Continue), - ("OC", Entry::Continue), - // Home > Alignment. - ("HAR", Entry::Finish(Action::Command("home.alignRight"))), - ("HAL", Entry::Finish(Action::Command("home.alignLeft"))), - ("HAC", Entry::Finish(Action::Command("home.alignCenter"))), - // Paste Special — ribbon and legacy routes. - ("HVS", Entry::Finish(Action::Dialog("pasteSpecial"))), - ("ES", Entry::Finish(Action::Dialog("pasteSpecial"))), - // AutoFit column width — ribbon and legacy routes. - ("HOI", Entry::Finish(Action::Command("home.autofitColumnWidth"))), - ("OCA", Entry::Finish(Action::Command("home.autofitColumnWidth"))), -]; - /// Look up a complete sequence. pub fn lookup(sequence: &str) -> Option { SEQUENCES.iter().find(|(seq, _)| *seq == sequence).map(|(_, entry)| *entry) @@ -79,7 +50,7 @@ pub fn is_group_level(seq: &str) -> bool { fn matches_target(action: &Action, target: &str) -> bool { match action { - Action::Command(id) => *id == target, + Action::Command(id) | Action::Pending(id) => *id == target, Action::Dialog(name) => format!("dialog:{name}") == target, Action::Tab(_) => false, } diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs new file mode 100644 index 0000000..446cbb2 --- /dev/null +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -0,0 +1,27 @@ +//! GENERATED from keytips.json by `cargo xtask keytips`. Edit the JSON, not this file. + +use super::{Action, Entry}; + +/// All known keytip sequences. Intermediate levels are implicit: the matcher waits +/// whenever a pressed letter starts a deeper sequence, so only terminal entries are +/// listed here. See keytip_table.rs for the helpers that read this table. +pub const SEQUENCES: &[(&str, Entry)] = &[ + ("H", Entry::Tab("Home")), + ("N", Entry::Tab("Insert")), + ("JI", Entry::Tab("Draw")), // Two-letter unit. Deeper Draw letters differ between Excel and GridCraft; left blank for future contributors. + ("P", Entry::Tab("Page Layout")), + ("M", Entry::Tab("Formulas")), + ("A", Entry::Tab("Data")), + ("R", Entry::Tab("Review")), + ("W", Entry::Tab("View")), + ("E", Entry::Tab("Home")), // Legacy Edit-menu path (Excel 2003); leads to the same Home menus. + ("O", Entry::Tab("Home")), // Legacy Format-menu path (Excel 2003); leads to the same Home menus. + ("HAR", Entry::Finish(Action::Command("home.alignRight"))), + ("HAL", Entry::Finish(Action::Command("home.alignLeft"))), + ("HAC", Entry::Finish(Action::Command("home.alignCenter"))), + ("HVS", Entry::Finish(Action::Dialog("pasteSpecial"))), + ("ES", Entry::Finish(Action::Dialog("pasteSpecial"))), // Legacy Edit menu. + ("HOI", Entry::Finish(Action::Command("home.autofitColumnWidth"))), + ("OCA", Entry::Finish(Action::Command("home.autofitColumnWidth"))), // Legacy Format menu. + ("WVG", Entry::Finish(Action::Command("view.gridlines"))), +]; diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json new file mode 100644 index 0000000..13489f1 --- /dev/null +++ b/crates/ui-egui/src/keytips/keytips.json @@ -0,0 +1,35 @@ +{ + "doc": [ + "Single source of truth for Excel-style sequential access keys (Alt+H+X+X style).", + "The generated keytip_table/generated.rs is produced from this file: `cargo xtask keytips`.", + "Adding or removing a shortcut is a one-line edit here. Rules:", + " - seq: the access-key sequence, letters and digits only (two-letter units like \"JI\" are one entry).", + " - kind: \"tab\" (switches a ribbon tab), \"command\" (runs a command id), \"dialog\" (opens a dialog by name),", + " \"continue\" (an explicit group level with no deeper sequences yet).", + " - Intermediate levels need no entries: the matcher waits automatically when deeper sequences exist.", + " - pending: true = the command does not exist yet; the sequence stays reserved and silent until it lands.", + " - Keep entries grouped by tab and sorted by sequence within a group; the FIRST entry for a tab is its canonical badge (legacy aliases come after it)." + ], + "sequences": [ + { "seq": "H", "kind": "tab", "target": "Home" }, + { "seq": "N", "kind": "tab", "target": "Insert" }, + { "seq": "JI", "kind": "tab", "target": "Draw", "note": "Two-letter unit. Deeper Draw letters differ between Excel and GridCraft; left blank for future contributors." }, + { "seq": "P", "kind": "tab", "target": "Page Layout" }, + { "seq": "M", "kind": "tab", "target": "Formulas" }, + { "seq": "A", "kind": "tab", "target": "Data" }, + { "seq": "R", "kind": "tab", "target": "Review" }, + { "seq": "W", "kind": "tab", "target": "View" }, + { "seq": "E", "kind": "tab", "target": "Home", "note": "Legacy Edit-menu path (Excel 2003); leads to the same Home menus." }, + { "seq": "O", "kind": "tab", "target": "Home", "note": "Legacy Format-menu path (Excel 2003); leads to the same Home menus." }, + + { "seq": "HAR", "kind": "command", "target": "home.alignRight" }, + { "seq": "HAL", "kind": "command", "target": "home.alignLeft" }, + { "seq": "HAC", "kind": "command", "target": "home.alignCenter" }, + { "seq": "HVS", "kind": "dialog", "target": "pasteSpecial" }, + { "seq": "ES", "kind": "dialog", "target": "pasteSpecial", "note": "Legacy Edit menu." }, + { "seq": "HOI", "kind": "command", "target": "home.autofitColumnWidth" }, + { "seq": "OCA", "kind": "command", "target": "home.autofitColumnWidth", "note": "Legacy Format menu." }, + + { "seq": "WVG", "kind": "command", "target": "view.gridlines" } + ] +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9c240e3..90287b7 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1015,6 +1015,9 @@ impl SheetApp { } keytips::Action::Command(id) => self.run_or_alert(id, json!({})), keytips::Action::Dialog(name) => self.open_dialog(name, json!({})), + // Reserved sequence: the command does not exist yet. Badge shows, invocation + // is silent (see keytips.json `pending`). + keytips::Action::Pending(_) => {} } } } diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index c684ddb..7ac66f7 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -298,7 +298,7 @@ fn held_keys_do_not_toggle_twice_or_leak_repeated_letters() { #[test] fn tab_letters_switch_to_their_ribbon_tabs() { let mut h = harness(blank()); - for (k, tab) in [(Key::N, "Insert"), (Key::P, "Page Layout"), (Key::M, "Formulas"), (Key::A, "Data"), (Key::R, "Review"), (Key::W, "View")] { + for (k, tab) in [(Key::N, "Insert"), (Key::P, "Page Layout"), (Key::M, "Formulas"), (Key::A, "Data"), (Key::R, "Review")] { key(&mut h, Key::F10); // arm letter(&mut h, k, false); assert_eq!(h.state().ui.ribbon_tab, tab, "letter {k:?}"); @@ -307,6 +307,28 @@ fn tab_letters_switch_to_their_ribbon_tabs() { } } +#[test] +fn view_gridlines_sequence_runs_end_to_end() { + let mut h = harness(blank()); + let gridlines = |h: &Harness| h.state().session.active().unwrap().wb.active().unwrap().show_gridlines; + assert!(gridlines(&h), "gridlines start visible"); + key(&mut h, Key::F10); + letter(&mut h, Key::W, false); + // W has deeper sequences: it stays as an active level so View badges show. + assert_eq!(h.state().ui.ribbon_tab, "View"); + assert_eq!(h.state().keytips.prefix(), Some("W")); + letter(&mut h, Key::V, false); + assert_eq!(h.state().keytips.prefix(), Some("WV"), "implicit group level waits for the last key"); + letter(&mut h, Key::G, false); + assert_eq!(h.state().keytips.prefix(), None, "the finished sequence clears the prefix"); + assert!(!gridlines(&h), "WVG toggles gridlines off"); + key(&mut h, Key::F10); + for k in [Key::W, Key::V, Key::G] { + letter(&mut h, k, false); + } + assert!(gridlines(&h), "WVG toggles gridlines back on"); +} + #[test] fn two_letter_unit_waits_after_first_letter_then_fires() { let mut h = harness(blank()); diff --git a/xtask/src/keytips.rs b/xtask/src/keytips.rs new file mode 100644 index 0000000..286a043 --- /dev/null +++ b/xtask/src/keytips.rs @@ -0,0 +1,111 @@ +//! Regenerate the keytip sequence table from `keytips.json`. +//! +//! `cargo xtask keytips` regenerate crates/ui-egui/src/keytips/generated.rs +//! `cargo xtask keytips --check` fail if the generated file differs from the JSON source + +use std::fs; +use std::path::Path; + +const TABS: &[&str] = &["Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"]; + +pub fn run(root: &Path, rest: &[&str]) -> Result<(), String> { + let json_path = root.join("crates/ui-egui/src/keytips/keytips.json"); + let out_path = root.join("crates/ui-egui/src/keytips/keytip_table/generated.rs"); + let generated = generate(&fs::read_to_string(&json_path).map_err(|e| format!("read {}: {e}", json_path.display()))?)?; + if rest.contains(&"--check") { + let on_disk = fs::read_to_string(&out_path).map_err(|e| format!("read {}: {e}", out_path.display()))?; + if on_disk != generated { + return Err(format!("{} is out of date — run `cargo xtask keytips` and commit the result", out_path.display())); + } + println!("keytips: {} up to date", out_path.display()); + return Ok(()); + } + fs::write(&out_path, &generated).map_err(|e| format!("write {}: {e}", out_path.display()))?; + println!("keytips: wrote {}", out_path.display()); + Ok(()) +} + +pub fn generate(source: &str) -> Result { + let doc: serde_json::Value = serde_json::from_str(source).map_err(|e| format!("invalid JSON: {e}"))?; + let seqs = doc.get("sequences").and_then(|v| v.as_array()).ok_or("missing `sequences` array")?; + + let mut rows: Vec<(String, String, String)> = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for (i, entry) in seqs.iter().enumerate() { + let at = format!("entry {}", i + 1); + let seq = entry.get("seq").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `seq`"))?; + if seq.is_empty() || seq.len() > 4 || !seq.bytes().all(|b| b.is_ascii_alphanumeric()) { + return Err(format!("{at}: seq {seq:?} must be 1-4 ASCII letters/digits")); + } + if !seen.insert(seq.to_string()) { + return Err(format!("{at}: duplicate seq {seq:?}")); + } + let kind = entry.get("kind").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `kind`"))?; + let target = entry.get("target").and_then(|v| v.as_str()).unwrap_or(""); + let pending = entry.get("pending").and_then(|v| v.as_bool()).unwrap_or(false); + let note = entry.get("note").and_then(|v| v.as_str()).unwrap_or(""); + let excel = entry.get("excel").and_then(|v| v.as_str()).unwrap_or(""); + if pending && kind != "command" { + return Err(format!("{at}: `pending` is only valid with kind `command`")); + } + let value = match kind { + "tab" => { + if target.is_empty() || !TABS.contains(&target) { + return Err(format!("{at}: tab target {target:?} is not one of {TABS:?}")); + } + format!("Entry::Tab({target:?})") + } + "command" => { + if target.is_empty() { + return Err(format!("{at}: command needs a `target` command id")); + } + if pending { format!("Entry::Finish(Action::Pending({target:?}))") } else { format!("Entry::Finish(Action::Command({target:?}))") } + } + "dialog" => { + if target.is_empty() { + return Err(format!("{at}: dialog needs a `target` dialog name")); + } + format!("Entry::Finish(Action::Dialog({target:?}))") + } + "continue" => "Entry::Continue".to_string(), + other => return Err(format!("{at}: unknown kind {other:?}")), + }; + let mut comment = String::new(); + if pending { + comment.push_str(" pending"); + } + if !excel.is_empty() { + if !comment.is_empty() { + comment.push(';'); + } + comment.push_str(&format!(" Excel: {excel}")); + } + if !note.is_empty() { + if !comment.is_empty() { + comment.push(';'); + } + comment.push_str(&format!(" {note}")); + } + rows.push((seq.to_string(), value, comment)); + } + // JSON order is authoritative and curated: the first entry for a tab is its canonical + // badge (legacy aliases come later), and groups stay together. Do not sort. + + let mut out = String::from( + "//! GENERATED from keytips.json by `cargo xtask keytips`. Edit the JSON, not this file.\n\n\ + use super::{Action, Entry};\n\n\ + /// All known keytip sequences. Intermediate levels are implicit: the matcher waits\n\ + /// whenever a pressed letter starts a deeper sequence, so only terminal entries are\n\ + /// listed here. See keytip_table.rs for the helpers that read this table.\n\ + pub const SEQUENCES: &[(&str, Entry)] = &[\n", + ); + for (seq, value, comment) in &rows { + out.push_str(&format!(" ({seq:?}, {value}),")); + if !comment.is_empty() { + out.push_str(&format!(" //{comment}")); + } + out.push('\n'); + } + out.push_str("];\n"); + Ok(out) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 603801f..14e9af3 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -7,6 +7,7 @@ mod assets; mod ico; +mod keytips; mod layers; #[cfg_attr(not(feature = "engine"), allow(dead_code))] mod parity; @@ -27,6 +28,8 @@ commands: wasm cargo check --target wasm32-unknown-unknown for the L0-L5 crates (+ gridcraft-web) parity [--check] [--out PATH] write docs/parity-checklist.md from gridcraft_engine::catalog (implemented vs missing ids) + keytips [--check] + regenerate crates/ui-egui/src/keytips/generated.rs from keytips.json (--check: fail if stale) version [set X.Y.Z[-pre]] print or set the workspace version ([workspace.package] in Cargo.toml) stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`) @@ -44,6 +47,7 @@ fn main() -> ExitCode { Some("ci") => cmd_ci(), Some("stats") => stats::run(&root(), rest.contains(&"--exact")), Some("parity") => parity::run(&root(), &rest), + Some("keytips") => keytips::run(&root(), &rest), Some("ico") => ico::run(&rest), Some("version") => version::run(&root(), &rest), Some("-h" | "--help" | "help") | None => { @@ -194,6 +198,7 @@ fn cmd_ci() -> Result<(), String> { } }), ), + ("keytips", Box::new(|| keytips::run(&root(), &["--check"]))), ]; let mut done = Vec::new(); for (name, f) in &steps { From 82cf8a6bb30bf30865ec6ef960e9409f53ed2b1c Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 19:05:19 +0300 Subject: [PATCH 04/15] Keytips: table-driven matcher and badge painting (issues #33, #49) Move the hardcoded keytip sequence list into keytip_table.rs as the single source of truth for both the matcher and the ribbon badge painter. - Generalize Action::Home into Action::Tab(&'static str); the legacy Edit/Format prefix special-case in lib.rs is absorbed into the table. - Badge helpers: tab_sequence (tabs), group_extension (menu anchors), item_letter (menu items), control_badges (command controls). - Add tab letters for Insert (N), Page Layout (P), Formulas (M), Data (A), Review (R), View (W); Draw/Automate stay unbadged until contextual letters land. - Tests: 5 table unit tests + tab_letters_switch_to_their_ribbon_tabs integration test. All 7 xtask ci gates green. --- crates/ui-egui/tests/keytips.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index 7ac66f7..7da90f5 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -298,12 +298,13 @@ fn held_keys_do_not_toggle_twice_or_leak_repeated_letters() { #[test] fn tab_letters_switch_to_their_ribbon_tabs() { let mut h = harness(blank()); - for (k, tab) in [(Key::N, "Insert"), (Key::P, "Page Layout"), (Key::M, "Formulas"), (Key::A, "Data"), (Key::R, "Review")] { + for (k, tab) in [(Key::N, "Insert"), (Key::P, "Page Layout"), (Key::M, "Formulas"), (Key::A, "Data"), (Key::R, "Review"), (Key::W, "View")] { key(&mut h, Key::F10); // arm letter(&mut h, k, false); assert_eq!(h.state().ui.ribbon_tab, tab, "letter {k:?}"); - assert_eq!(h.state().keytips.prefix(), None, "terminal tab units finish cleanly"); assert!(h.state().editor.is_none(), "tab letters must not begin a cell edit"); + key(&mut h, Key::F10); // disarm + assert_eq!(h.state().keytips.prefix(), None); } } From f11d74ee7ca14de58e75ea236a0623194d1794f0 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 19:53:05 +0300 Subject: [PATCH 05/15] Populate the full Excel keytip table (408 sequences, issues #33 and #49) Population of keytips.json from the collected Excel Alt-sequence set: - 408 curated entries: 10 tabs, 354 command binds (64 reserved pending, 290 live catalog commands), 2 dialog openers, 42 group levels. - Bind-all policy: sequences whose command does not exist yet bind to the canonical future id as Action::Pending -- badge shows, invocation is a silent no-op until the command lands (ids from docs/parity-checklist). - Group levels: 46 command entries that are strict prefixes of deeper sequences were demoted to menu openers so the trie never fires early (HV must not paste; it waits for the variant letter). - Fire-immediately restores: HC Copy, HAN Accounting, FA Save As, FO Open, FI Info. Unreachable legacy aliases (HCC, HANM, FA1, FAY3, FOC, FOO, FIP) dropped: the parent fires first. - ES stays a terminal dialog opener as in Excel: the A/F/T/V variants are dialog mnemonics, not trie sequences. - Escape pops one trie level and returns to the armed root at a single letter; keytips.json is the single modifiable source; xtask keytips now emits through rustfmt so --check is byte-stable. - Tests: 99 pass (27 lib + 14 keytips + suite), all 8 CI gates green. --- crates/ui-egui/src/keytips.rs | 12 +- crates/ui-egui/src/keytips/keytip_table.rs | 2 +- .../src/keytips/keytip_table/generated.rs | 392 ++- crates/ui-egui/src/keytips/keytips.json | 2863 ++++++++++++++++- crates/ui-egui/tests/keytips.rs | 7 + xtask/src/keytips.rs | 30 +- 6 files changed, 3276 insertions(+), 30 deletions(-) diff --git a/crates/ui-egui/src/keytips.rs b/crates/ui-egui/src/keytips.rs index 85bb214..51a8793 100644 --- a/crates/ui-egui/src/keytips.rs +++ b/crates/ui-egui/src/keytips.rs @@ -92,10 +92,14 @@ impl KeyTips { } else if self.prefix.is_some() && !matches!(key, Key::ShiftLeft | Key::ShiftRight) { handled = true; if *key == Key::Escape { - if let Some(prefix) = self.prefix.as_mut() - && prefix.pop().is_none() - { - self.prefix = None; + // Escape pops one level; at a single-letter level it + // returns to the armed root (empty prefix). + if let Some(prefix) = self.prefix.as_mut() { + if prefix.is_empty() { + self.prefix = None; + } else { + prefix.pop(); + } } } else if key.name().len() == 1 { let next = format!("{}{key_name}", self.prefix.as_deref().unwrap_or_default(), key_name = key.name()); diff --git a/crates/ui-egui/src/keytips/keytip_table.rs b/crates/ui-egui/src/keytips/keytip_table.rs index 638b6dd..8caa4d6 100644 --- a/crates/ui-egui/src/keytips/keytip_table.rs +++ b/crates/ui-egui/src/keytips/keytip_table.rs @@ -149,7 +149,7 @@ mod tests { #[test] fn item_letter_is_one_level_deep() { assert_eq!(item_letter("HV", "dialog:pasteSpecial"), Some("HVS")); - assert_eq!(item_letter("E", "dialog:pasteSpecial"), Some("ES")); + assert_eq!(item_letter("HV", "edit.pasteSpecial"), Some("HVA")); assert_eq!(item_letter("H", "dialog:pasteSpecial"), None, "the item badge waits for its group letter"); assert_eq!(item_letter("HO", "home.autofitColumnWidth"), Some("HOI")); } diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index 446cbb2..c078e77 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -20,8 +20,398 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HAL", Entry::Finish(Action::Command("home.alignLeft"))), ("HAC", Entry::Finish(Action::Command("home.alignCenter"))), ("HVS", Entry::Finish(Action::Dialog("pasteSpecial"))), - ("ES", Entry::Finish(Action::Dialog("pasteSpecial"))), // Legacy Edit menu. + ("ES", Entry::Finish(Action::Dialog("pasteSpecial"))), // Excel: Classic Paste Special dialog (legacy Edit route); A/F/T/V are dialog mnemonics ("HOI", Entry::Finish(Action::Command("home.autofitColumnWidth"))), ("OCA", Entry::Finish(Action::Command("home.autofitColumnWidth"))), // Legacy Format menu. ("WVG", Entry::Finish(Action::Command("view.gridlines"))), + ("0", Entry::Finish(Action::Pending("qat.10"))), // pending; Excel: Quick Access Toolbar position 10; QAT navigation does not exist yet + ("1", Entry::Finish(Action::Pending("qat.1"))), // pending; Excel: Quick Access Toolbar position 1; QAT navigation does not exist yet + ("2", Entry::Finish(Action::Pending("qat.2"))), // pending; Excel: Quick Access Toolbar position 2; QAT navigation does not exist yet + ("3", Entry::Finish(Action::Pending("qat.3"))), // pending; Excel: Quick Access Toolbar position 3; QAT navigation does not exist yet + ("4", Entry::Finish(Action::Pending("qat.4"))), // pending; Excel: Quick Access Toolbar position 4; QAT navigation does not exist yet + ("5", Entry::Finish(Action::Pending("qat.5"))), // pending; Excel: Quick Access Toolbar position 5; QAT navigation does not exist yet + ("H0", Entry::Finish(Action::Command("home.increaseDecimal"))), // Excel: Increase Decimals; containment(Increase Decimal) + ("H1", Entry::Finish(Action::Command("home.bold"))), // Excel: Bold + ("H2", Entry::Finish(Action::Command("home.italic"))), // Excel: Italic + ("H3", Entry::Continue), // Excel: Underline Menu; group level: + ("H3D", Entry::Finish(Action::Command("home.doubleUnderline"))), // Excel: Double Underline + ("H3U", Entry::Finish(Action::Command("home.underline"))), // Excel: Underline + ("H5", Entry::Finish(Action::Command("home.decreaseIndent"))), // Excel: Decrease Indent + ("H6", Entry::Finish(Action::Command("home.increaseIndent"))), // Excel: Increase Indent + ("H9", Entry::Finish(Action::Command("home.decreaseDecimal"))), // Excel: Decrease Decimals; containment(Decrease Decimal) + ("HAN", Entry::Finish(Action::Command("home.accounting"))), // Excel: Accounting Format; fires immediately in Excel; deeper aliases dropped as unreachable + ("HAS", Entry::Finish(Action::Command("home.addIns"))), // Excel: Add-ins; containment(Add-ins) + ("HB", Entry::Continue), // Excel: Borders Menu; group level: + ("HBA", Entry::Finish(Action::Command("home.borders"))), // Excel: All Borders; containment(Borders) + ("HBB", Entry::Finish(Action::Command("home.borders"))), // Excel: Bottom Double Border; border-side TBD + ("HBC", Entry::Finish(Action::Command("home.borders"))), // Excel: Top and Thick Bottom Border; border-side TBD + ("HBD", Entry::Finish(Action::Command("home.borders"))), // Excel: Top and Bottom Border; border-side TBD + ("HBE", Entry::Finish(Action::Command("home.borders"))), // Excel: Erase Border; border-side TBD + ("HBG", Entry::Finish(Action::Command("home.drawBorder"))), // Excel: Draw Border Grid; containment(Draw Border) + ("HBH", Entry::Finish(Action::Command("home.borders"))), // Excel: Thick Bottom Border; border-side TBD + ("HBI", Entry::Finish(Action::Command("home.borders"))), // Excel: Line Color; Line Color (submenu TBD) + ("HBL", Entry::Finish(Action::Command("home.borders"))), // Excel: Left Border; border-side TBD + ("HBM", Entry::Finish(Action::Command("home.borders"))), // Excel: More Borders; containment(Borders) + ("HBN", Entry::Finish(Action::Command("home.borders"))), // Excel: No Border; border-side TBD + ("HBO", Entry::Finish(Action::Command("home.borders"))), // Excel: Bottom Border; border-side TBD + ("HBP", Entry::Finish(Action::Command("home.borders"))), // Excel: Top Border; border-side TBD + ("HBR", Entry::Finish(Action::Command("home.borders"))), // Excel: Right Border; border-side TBD + ("HBS", Entry::Finish(Action::Command("home.borders"))), // Excel: Outside Borders; containment(Borders) + ("HBT", Entry::Finish(Action::Command("home.borders"))), // Excel: Thick Outside Borders; border-side TBD + ("HBU", Entry::Finish(Action::Command("home.borders"))), // Excel: Top and Double Bottom Border; border-side TBD + ("HBW", Entry::Finish(Action::Command("home.drawBorder"))), // Excel: Draw Border + ("HBY", Entry::Finish(Action::Command("home.borders"))), // Excel: Line Style; Line Style (submenu TBD) + ("HC", Entry::Finish(Action::Command("edit.copy"))), // Excel: Copy; fires immediately in Excel; deeper aliases dropped as unreachable + ("HCP", Entry::Finish(Action::Pending("home.copyAsPicture"))), // pending; Excel: Copy as Picture; reserved until the command lands + ("HDC", Entry::Finish(Action::Command("home.deleteColumns"))), // Excel: Delete Columns; Delete Columns + ("HDD", Entry::Finish(Action::Command("home.deleteCells"))), // Excel: Delete Cells + ("HDR", Entry::Finish(Action::Command("home.deleteRows"))), // Excel: Delete Rows; Delete Rows + ("HDS", Entry::Finish(Action::Command("home.deleteSheet"))), // Excel: Delete Sheet + ("HEA", Entry::Finish(Action::Command("edit.clearAll"))), // Excel: Clear All + ("HEC", Entry::Finish(Action::Command("edit.clearContents"))), // Excel: Clear Contents + ("HEF", Entry::Finish(Action::Command("edit.clearFormats"))), // Excel: Clear Formats + ("HEL", Entry::Finish(Action::Command("edit.clearHyperlinks"))), // Excel: Clear Hyperlinks + ("HEM", Entry::Finish(Action::Command("edit.clearComments"))), // Excel: Clear Comments and Notes; containment(Clear Comments and Notes) + ("HFC", Entry::Finish(Action::Command("home.fontColor"))), // Excel: Font Color + ("HFDC", Entry::Finish(Action::Command("home.conditionalFormat"))), // Excel: Find Conditional Formatting; containment(Conditional Formatting) + ("HFDF", Entry::Finish(Action::Command("edit.find"))), // Excel: Find + ("HFDG", Entry::Finish(Action::Command("edit.goTo"))), // Excel: Go To + ("HFDM", Entry::Finish(Action::Command("edit.find"))), // Excel: Find Notes; containment(Find) + ("HFDO", Entry::Finish(Action::Pending("home.selectObjects"))), // pending; Excel: Select Objects; Select Objects; reserved until the command lands + ("HFDP", Entry::Finish(Action::Command("arrange.selectionPane"))), // Excel: Selection Pane + ("HFDR", Entry::Finish(Action::Command("edit.replace"))), // Excel: Replace + ("HFDS", Entry::Finish(Action::Command("edit.goToSpecial"))), // Excel: Go To Special + ("HFF", Entry::Finish(Action::Command("home.fontName"))), // Excel: Font Selection + ("HFG", Entry::Finish(Action::Command("home.increaseFontSize"))), // Excel: Increase Font Size + ("HFI", Entry::Continue), // Excel: Fill; group level: containment(Fill Up) + ("HFIA", Entry::Finish(Action::Pending("home.fillAcrossWorksheets"))), // pending; Excel: Fill Across Worksheets; Fill Across Worksheets; reserved until the command lands + ("HFID", Entry::Finish(Action::Command("edit.fillDown"))), // Excel: Fill Down + ("HFIF", Entry::Finish(Action::Command("edit.flashFill"))), // Excel: Flash Fill + ("HFIJ", Entry::Finish(Action::Command("home.justify"))), // Excel: Fill Justify; containment(Justify) + ("HFIL", Entry::Finish(Action::Command("edit.fillLeft"))), // Excel: Fill Left + ("HFIR", Entry::Finish(Action::Command("edit.fillRight"))), // Excel: Fill Right + ("HFIS", Entry::Finish(Action::Command("edit.fillSeries"))), // Excel: Fill Series; containment(Series) + ("HFIU", Entry::Finish(Action::Command("edit.fillUp"))), // Excel: Fill Up + ("HFK", Entry::Finish(Action::Command("home.decreaseFontSize"))), // Excel: Decrease Font Size + ("HFM", Entry::Finish(Action::Command("home.numberFormat"))), // Excel: More Number Formats; containment(Number Format) + ("HFP", Entry::Finish(Action::Command("edit.formatPainter"))), // Excel: Format Painter + ("HFQ", Entry::Continue), // Excel: Orientation Menu; group level: containment(Orientation) + ("HFQL", Entry::Finish(Action::Pending("home.angleClockwise"))), // pending; Excel: Angle Clockwise; Angle Clockwise; reserved until the command lands + ("HFQO", Entry::Finish(Action::Pending("home.angleCounterclockwise"))), // pending; Excel: Angle Counterclockwise; Angle Counterclockwise; reserved until the command lands + ("HFS", Entry::Finish(Action::Command("home.fontSize"))), // Excel: Font Size + ("HH", Entry::Finish(Action::Command("home.fillColor"))), // Excel: Fill Color + ("HIC", Entry::Finish(Action::Command("home.insertColumns"))), // Excel: Insert Columns; Insert Columns + ("HIE", Entry::Finish(Action::Command("home.insertCells"))), // Excel: Insert Copied Cells; Insert Cells (copied cells TBD) + ("HII", Entry::Finish(Action::Command("home.insertCells"))), // Excel: Insert Cells + ("HIR", Entry::Finish(Action::Command("home.insertRows"))), // Excel: Insert Rows; Insert Rows + ("HIS", Entry::Finish(Action::Command("home.insertSheet"))), // Excel: Insert Sheet + ("HJ", Entry::Continue), // Excel: Cell Styles; group level: + ("HJN", Entry::Finish(Action::Command("home.newCellStyle"))), // Excel: New Cell Style + ("HK", Entry::Finish(Action::Command("home.comma"))), // Excel: Comma Style + ("HL", Entry::Continue), // Excel: Conditional Formatting; group level: containment(Conditional Formatting) + ("HLC", Entry::Continue), // Excel: Clear Rules Menu; group level: containment(Clear Rules) + ("HLCE", Entry::Finish(Action::Command("home.clearRules"))), // Excel: Clear Rules From Sheet; containment(Clear Rules) + ("HLCS", Entry::Finish(Action::Command("home.clearRules"))), // Excel: Clear Rules From Cells; containment(Clear Rules) + ("HLCT", Entry::Finish(Action::Command("home.clearRules"))), // Excel: Clear Rules From Table; containment(Clear Rules) + ("HLD", Entry::Finish(Action::Command("home.conditionalFormat"))), // Excel: Data Bars; Data Bars (submenu TBD) + ("HLH", Entry::Finish(Action::Command("home.conditionalFormat"))), // Excel: Highlight Cells; Highlight Cells (submenu TBD) + ("HLN", Entry::Finish(Action::Command("home.conditionalFormat"))), // Excel: New Rule; New Rule (submenu TBD) + ("HLR", Entry::Finish(Action::Command("home.manageRules"))), // Excel: Manage Rules; containment(Manage Rules) + ("HLT", Entry::Finish(Action::Command("home.conditionalFormat"))), // Excel: Top/Bottom Rules; Top/Bottom (submenu TBD) + ("HM", Entry::Continue), // Excel: Merge & Center Menu; group level: containment(Merge & Center) + ("HMA", Entry::Finish(Action::Command("home.mergeAcross"))), // Excel: Merge Across + ("HMC", Entry::Finish(Action::Command("home.mergeCenter"))), // Excel: Merge & Center + ("HMM", Entry::Finish(Action::Command("home.mergeCells"))), // Excel: Merge Cells + ("HMU", Entry::Finish(Action::Command("home.unmergeCells"))), // Excel: Unmerge Cells + ("HN", Entry::Finish(Action::Command("home.numberFormat"))), // Excel: Number Format + ("HOA", Entry::Finish(Action::Command("home.autofitRowHeight"))), // Excel: AutoFit Row Height + ("HOD", Entry::Finish(Action::Command("home.columnWidth"))), // Excel: Default Column Width; containment(Column Width) + ("HOE", Entry::Finish(Action::Command("home.formatCells"))), // Excel: Format Cells + ("HOH", Entry::Finish(Action::Command("home.rowHeight"))), // Excel: Row Height + ("HOL", Entry::Finish(Action::Command("home.lockCell"))), // Excel: Lock Cell; containment(Lock Cell) + ("HOM", Entry::Finish(Action::Command("sheet.move"))), // Excel: Move or Copy Sheet + ("HOP", Entry::Finish(Action::Command("review.protectSheet"))), // Excel: Protect Sheet + ("HOR", Entry::Finish(Action::Command("sheet.rename"))), // Excel: Rename Sheet + ("HOT", Entry::Finish(Action::Command("sheet.tabColor"))), // Excel: Tab Color + ("HOUC", Entry::Finish(Action::Command("home.hideColumns"))), // Excel: Hide Columns + ("HOUH", Entry::Finish(Action::Command("sheet.unhide"))), // Excel: Unhide Sheet + ("HOUL", Entry::Finish(Action::Command("home.unhideColumns"))), // Excel: Unhide Columns + ("HOUO", Entry::Finish(Action::Command("home.unhideRows"))), // Excel: Unhide Rows + ("HOUR", Entry::Finish(Action::Command("home.hideRows"))), // Excel: Hide Rows + ("HOUS", Entry::Finish(Action::Command("sheet.hide"))), // Excel: Hide Sheet + ("HOW", Entry::Finish(Action::Command("home.columnWidth"))), // Excel: Column Width + ("HP", Entry::Finish(Action::Command("home.numberFormat"))), // Excel: Percentage Style; format TBD + ("HSC", Entry::Finish(Action::Command("data.clearFilter"))), // Excel: Clear Filter + ("HSF", Entry::Finish(Action::Command("data.filter"))), // Excel: Filter + ("HSO", Entry::Finish(Action::Command("data.sortDescending"))), // Excel: Sort Z to A + ("HSS", Entry::Finish(Action::Command("data.sortAscending"))), // Excel: Sort A to Z + ("HSU", Entry::Finish(Action::Command("data.sort"))), // Excel: Custom Sort + ("HT", Entry::Continue), // Excel: Format as Table; group level: + ("HTN", Entry::Finish(Action::Command("table.style"))), // Excel: New Table Style + ("HTP", Entry::Finish(Action::Command("table.style"))), // Excel: New Pivot Table Style + ("HU", Entry::Continue), // Excel: AutoSum; group level: + ("HUA", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Auto Average; AutoSum: Average (variant TBD) + ("HUC", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Auto Count Numbers; AutoSum: Count (variant TBD) + ("HUI", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Auto Min; AutoSum: Min (variant TBD) + ("HUM", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Auto Max; AutoSum: Max (variant TBD) + ("HV", Entry::Continue), // Excel: Paste; group level: + ("HVA", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Values and Number Formatting; paste variant, params TBD: what=TBD(values+numfmt) + ("HVB", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste No Borders; paste variant, params TBD: what=TBD(no-borders) + ("HVE", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Values and Source Formatting; paste variant, params TBD: what=TBD(values+src-fmt) + ("HVF", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Formula; paste-variant TBD + ("HVI", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Linked Picture; what=TBD(linked-picture) + ("HVK", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Keep Source Formatting; paste variant, params TBD: what=TBD(keep-source) + ("HVM", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Destination Formatting; paste variant, params TBD: what=TBD(dest) + ("HVN", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Link; paste variant, params TBD: what=TBD(link) + ("HVO", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Formula and Formatting; paste variant, params TBD: what=TBD(formula+formatting) + ("HVP", Entry::Finish(Action::Command("edit.paste"))), // Excel: Paste + ("HVR", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Formatting; paste variant, params TBD: what=formats + ("HVT", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Transpose; paste variant, params TBD: what=transpose + ("HVU", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Picture; what=TBD(picture) + ("HVV", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Values; paste variant, params TBD: what=values + ("HVW", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Keep Source Column Widths; paste variant, params TBD: what=TBD(col-widths) + ("HW", Entry::Continue), // Excel: Wrap Text Menu; group level: containment(Wrap Text) + ("HWS", Entry::Finish(Action::Pending("home.shrinkToFit"))), // pending; Excel: Shrink to Fit; Shrink to Fit; reserved until the command lands + ("HWW", Entry::Finish(Action::Command("home.wrapText"))), // Excel: Wrap Text + ("HX", Entry::Finish(Action::Command("edit.cut"))), // Excel: Cut + ("NC1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Column Chart; Column chart (kind TBD) + ("NC2", Entry::Finish(Action::Command("review.newComment"))), // Excel: New Comment + ("NCB", Entry::Finish(Action::Command("insert.checkbox"))), // Excel: Checkbox + ("ND", Entry::Finish(Action::Command("insert.chart"))), // Excel: Scatter Chart; Scatter (kind TBD) + ("NE", Entry::Finish(Action::Pending("insert.equation"))), // pending; Excel: Equations; Equations; reserved until the command lands + ("NF", Entry::Continue), // Excel: Online Pictures; group level: containment(Picture) + ("NFP", Entry::Continue), // Excel: From Picture Menu; group level: containment(Picture) + ("NFPF", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture From File; containment(Picture) + ("NH1", Entry::Finish(Action::Command("pageLayout.headerFooter"))), // Excel: Header & Footer + ("NHF", Entry::Finish(Action::Command("pageLayout.headerFooter"))), // Excel: Header & Footer + ("NHI", Entry::Finish(Action::Command("insert.chart"))), // Excel: Hierarchy Chart; Hierarchy (kind TBD) + ("NI1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Waterfall Chart; Waterfall (kind TBD) + ("NI2", Entry::Finish(Action::Command("insert.link"))), // Excel: Link + ("NJ", Entry::Finish(Action::Pending("`insert.object`"))), // pending; Excel: Object; insert.object missing; reserved until the command lands + ("NM1T", Entry::Finish(Action::Command("insert.picture"))), // Excel: SmartArt Picture; containment(Picture) + ("NM2", Entry::Finish(Action::Command("insert.chart"))), // Excel: Map Chart; Map (kind TBD) + ("NN1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Line Chart; Line chart (kind TBD) + ("NNS", Entry::Finish(Action::Command("insert.icons"))), // Excel: Icons + ("NP", Entry::Continue), // Excel: Picture; group level: + ("NPB", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from File; containment(Picture) + ("NPF", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place in Cell; Picture in cell (source TBD) + ("NPI", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place in Cell Menu; Picture in cell (source TBD) + ("NPO", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place over Cells Menu; Picture over cells (source TBD) + ("NQ", Entry::Finish(Action::Command("insert.chart"))), // Excel: Pie Chart; Pie (kind TBD) + ("NR", Entry::Finish(Action::Command("insert.recommendedCharts"))), // Excel: Recommended Charts + ("NSA", Entry::Finish(Action::Command("insert.chart"))), // Excel: Statistical Chart; Statistical chart (kind TBD) + ("NSC", Entry::Finish(Action::Pending("insert.screenshot"))), // pending; Excel: Screenshot Menu; Screenshot; reserved until the command lands + ("NSD", Entry::Finish(Action::Command("insert.chart"))), // Excel: Combo Chart; Combo (kind TBD) + ("NSF", Entry::Finish(Action::Pending("`insert.slicer`"))), // pending; Excel: Slicer; insert.slicer missing (gap #8); reserved until the command lands + ("NSH", Entry::Finish(Action::Command("insert.shape"))), // Excel: Shapes + ("NSP", Entry::Finish(Action::Command("insert.recommendedPivotTables"))), // Excel: Recommended PivotTables + ("NST", Entry::Finish(Action::Pending("`insert.timeline`"))), // pending; Excel: Timeline; insert.timeline missing (gap #8); reserved until the command lands + ("NSZ", Entry::Finish(Action::Pending("insert.pivotChart"))), // pending; Excel: PivotChart; PivotChart; reserved until the command lands + ("NT", Entry::Finish(Action::Command("insert.table"))), // Excel: Table + ("NU", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Symbols; containment(Symbol) + ("NV", Entry::Finish(Action::Command("insert.pivotTable"))), // Excel: PivotTable + ("NW", Entry::Finish(Action::Pending("insert.wordArt"))), // pending; Excel: WordArt; WordArt; reserved until the command lands + ("NX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box + ("NZK", Entry::Continue), // Excel: Sparklines Menu; group level: + ("NZKSL", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Line Sparkline + ("NZKSO", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Column Sparkline + ("NZKSW", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Win/Loss Sparkline + ("NZTX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box + ("PAE", Entry::Finish(Action::Command("arrange.sendBackward"))), // Excel: Send Backward + ("PAF", Entry::Finish(Action::Command("arrange.bringForward"))), // Excel: Bring Forward + ("PB", Entry::Continue), // Excel: Breaks; group level: + ("PBI", Entry::Finish(Action::Command("pageLayout.breaks"))), // Excel: Insert Page Break; Insert Page Break + ("PBR", Entry::Finish(Action::Command("pageLayout.breaks"))), // Excel: Remove Page Break; Remove Page Break + ("PG", Entry::Finish(Action::Command("pageLayout.background"))), // Excel: Background + ("PI", Entry::Finish(Action::Command("pageLayout.printTitles"))), // Excel: Print Titles + ("PM", Entry::Continue), // Excel: Margins; group level: + ("PMA", Entry::Finish(Action::Command("pageLayout.margins"))), // Excel: Custom Margins; containment(Margins) + ("PO", Entry::Finish(Action::Command("pageLayout.orientation"))), // Excel: Orientation + ("PPG", Entry::Finish(Action::Command("pageLayout.printGridlines"))), // Excel: Print Gridlines + ("PPH", Entry::Finish(Action::Command("pageLayout.printHeadings"))), // Excel: Print Headings + ("PR", Entry::Continue), // Excel: Print Area; group level: + ("PRS", Entry::Finish(Action::Command("pageLayout.printArea"))), // Excel: Set Print Area + ("PSP", Entry::Finish(Action::Command("pageLayout.pageSetup"))), // Excel: Page Setup + ("PSZ", Entry::Finish(Action::Command("pageLayout.size"))), // Excel: Size + ("PTC", Entry::Finish(Action::Command("pageLayout.themeColors"))), // Excel: Theme Colors; containment(Colors) + ("PTE", Entry::Finish(Action::Command("pageLayout.themeEffects"))), // Excel: Theme Effects; containment(Effects) + ("PTF", Entry::Finish(Action::Command("pageLayout.themeFonts"))), // Excel: Theme Fonts; containment(Fonts) + ("PTH", Entry::Continue), // Excel: Themes; group level: + ("PTHB", Entry::Finish(Action::Command("pageLayout.theme"))), // Excel: Browse Themes; containment(Themes) + ("PVG", Entry::Finish(Action::Command("view.gridlines"))), // Excel: View Gridlines + ("PVH", Entry::Finish(Action::Command("view.headings"))), // Excel: View Headings + ("MA", Entry::Continue), // Excel: Remove Arrows Menu; group level: containment(Remove Arrows) + ("MAA", Entry::Finish(Action::Pending("formulas.removeArrows"))), // pending; Excel: Remove All Arrows; Remove All Arrows; reserved until the command lands + ("MAP", Entry::Finish(Action::Pending("formulas.removePrecedentArrows"))), // pending; Excel: Remove Precedent Arrows; Remove Precedent Arrows; reserved until the command lands + ("MB", Entry::Finish(Action::Command("formulas.calculateNow"))), // Excel: Calculate Now + ("MC", Entry::Finish(Action::Command("formulas.createFromSelection"))), // Excel: Create From Selection + ("MD", Entry::Finish(Action::Command("formulas.traceDependents"))), // Excel: Trace Dependents + ("ME", Entry::Continue), // Excel: Date & Time Menu; group level: containment(Date & Time) + ("MEF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Date & Time - Insert Function; containment(Insert Function) + ("MF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Insert Function + ("MG", Entry::Continue), // Excel: Math & Trig Menu; group level: containment(Math & Trig) + ("MGF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Math & Trig - Insert Function; containment(Insert Function) + ("MH", Entry::Finish(Action::Command("formulas.showFormulas"))), // Excel: Show Formulas + ("MI", Entry::Continue), // Excel: Financial Menu; group level: containment(Financial) + ("MIF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Financial - Insert Function; containment(Insert Function) + ("MJ", Entry::Finish(Action::Command("formulas.calculateSheet"))), // Excel: Calculate Sheet + ("MK", Entry::Continue), // Excel: Error Checking Menu; group level: containment(Error Checking) + ("MKK", Entry::Finish(Action::Command("formulas.errorChecking"))), // Excel: Error Checking + ("ML", Entry::Continue), // Excel: Logical Menu; group level: containment(Logical) + ("MLF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Logical - Insert Function; containment(Insert Function) + ("MM", Entry::Finish(Action::Command("formulas.defineName"))), // Excel: Define Name + ("MN", Entry::Finish(Action::Command("formulas.nameManager"))), // Excel: Name Manager + ("MO", Entry::Continue), // Excel: Lookup & Reference Menu; group level: containment(Lookup & Reference) + ("MOF", Entry::Finish(Action::Command("formulas.lookupReference"))), // Excel: Lookup & Reference - Insert Function; containment(Lookup & Reference) + ("MP", Entry::Finish(Action::Command("formulas.tracePrecedents"))), // Excel: Trace Precedents + ("MQ", Entry::Continue), // Excel: More Functions Menu; group level: containment(More Functions) + ("MQCF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Cube - Insert Function; containment(Insert Function) + ("MQEF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Engineering - Insert Function; containment(Insert Function) + ("MQIF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Information - Insert Function; containment(Insert Function) + ("MQOF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Compatibility - Insert Function; containment(Insert Function) + ("MQSF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Statistical - Insert Function; containment(Insert Function) + ("MQWF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Web - Insert Function; containment(Insert Function) + ("MR", Entry::Continue), // Excel: Recently Used Menu; group level: containment(Recently Used) + ("MRF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Recently Used - Insert Function; containment(Insert Function) + ("MS", Entry::Continue), // Excel: Use in Formula Menu; group level: containment(Use in Formula) + ("MSP", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Names; paste-variant TBD + ("MT", Entry::Continue), // Excel: Text Menu; group level: containment(Text) + ("MTF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Text - Insert Function; containment(Insert Function) + ("MU", Entry::Continue), // Excel: AutoSum Menu; group level: + ("MUA", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Average; AutoSum: Average (variant TBD) + ("MUC", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Count Numbers; AutoSum: Count (variant TBD) + ("MUF", Entry::Finish(Action::Command("formulas.moreFunctions"))), // Excel: More Functions (AutoSum); containment(More Functions) + ("MUI", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Min; AutoSum: Min (variant TBD) + ("MUM", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Max; AutoSum: Max (variant TBD) + ("MUS", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Sum + ("MV", Entry::Finish(Action::Command("formulas.evaluateFormula"))), // Excel: Evaluate Formula + ("MW", Entry::Finish(Action::Command("formulas.watchWindow"))), // Excel: Watch Window + ("MX", Entry::Continue), // Excel: Calculation Options Menu; group level: containment(Calculation Options) + ("MXA", Entry::Finish(Action::Command("formulas.calculationOptions"))), // Excel: Automatic Calculation; Calculation options: Automatic + ("MXM", Entry::Finish(Action::Command("formulas.calculationOptions"))), // Excel: Manual Calculation; Calculation options: Manual + ("MXP", Entry::Finish(Action::Command("formulas.calculationOptions"))), // Excel: Partial Calculation; Calculation options: Partial + ("AB", Entry::Finish(Action::Command("data.subtotal"))), // Excel: Subtotal + ("AC", Entry::Finish(Action::Command("data.clearFilter"))), // Excel: Clear Filter + ("AD", Entry::Finish(Action::Command("data.consolidate"))), // Excel: Consolidate + ("AE", Entry::Finish(Action::Command("data.textToColumns"))), // Excel: Text to Columns + ("AF1", Entry::Continue), // Excel: Data from Picture Menu; group level: feature gap; reserved until the command lands + ("AF1C", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from Clipboard (source TBD) + ("AF1P", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from File (source TBD) + ("AFA", Entry::Finish(Action::Pending("data.fromAccess"))), // pending; Excel: Get External Data: From Access; gap #12 Get & Transform; reserved until the command lands + ("AFC", Entry::Finish(Action::Pending("`data.forecastSheet`"))), // pending; Excel: Forecast Sheet; IN FLIGHT: vangel76 PR #325; reserved until the command lands + ("AFE", Entry::Finish(Action::Pending("`data.analyzeData`"))), // pending; Excel: Analyze Data; AI feature GAP; reserved until the command lands + ("AFF", Entry::Finish(Action::Command("edit.flashFill"))), // Excel: Flash Fill + ("AFO", Entry::Finish(Action::Pending("data.fromOtherSources"))), // pending; Excel: Get External Data: From Other Sources; gap #12; reserved until the command lands + ("AFT", Entry::Finish(Action::Pending("data.fromText"))), // pending; Excel: Get External Data: From Text; gap #12; reserved until the command lands + ("AFW", Entry::Finish(Action::Pending("data.fromWeb"))), // pending; Excel: Get External Data: From Web; reserved until the command lands + ("AG", Entry::Continue), // Excel: Group Menu; group level: containment(Group) + ("AGA", Entry::Finish(Action::Pending("data.autoOutline"))), // pending; Excel: Auto Outline; Auto Outline; reserved until the command lands + ("AGG", Entry::Finish(Action::Command("data.group"))), // Excel: Group + ("AH", Entry::Finish(Action::Command("data.hideDetail"))), // Excel: Hide Detail + ("AJ", Entry::Finish(Action::Command("data.showDetail"))), // Excel: Show Detail + ("AK", Entry::Finish(Action::Command("data.queriesConnections"))), // Excel: Edit Links; Edit Links (Queries & Connections); + ("AM", Entry::Finish(Action::Command("data.removeDuplicates"))), // Excel: Remove Duplicates + ("AO", Entry::Finish(Action::Command("data.queriesConnections"))), // Excel: Queries & Connections + ("AP1", Entry::Finish(Action::Command("file.properties"))), // Excel: Properties + ("APN", Entry::Continue), // Excel: Get Data (Power Query) Menu; group level: Power Query variants = GAP + ("APND", Entry::Finish(Action::Pending("data.fromDatabase"))), // pending; Excel: From Database (Microsoft Query); gap #12; reserved until the command lands + ("APNE", Entry::Finish(Action::Pending("data.powerQueryEditor"))), // pending; Excel: Launch Power Query Editor; gap #12; reserved until the command lands + ("APNG", Entry::Finish(Action::Command("data.getData"))), // Excel: Get Data (Power Query); containment(Get Data) + ("APNH", Entry::Finish(Action::Pending("data.fromHtml"))), // pending; Excel: From HTML; gap #12; reserved until the command lands + ("APNL", Entry::Finish(Action::Pending("data.fromSqlServer"))), // pending; Excel: From SQL Server ODBC; gap #12; reserved until the command lands + ("APNS", Entry::Finish(Action::Pending("data.dataSourceSettings"))), // pending; Excel: Data Source Settings; gap #12; reserved until the command lands + ("APNT", Entry::Finish(Action::Pending("data.fromTextLegacy"))), // pending; Excel: From Text (Legacy); gap #12; reserved until the command lands + ("APR", Entry::Finish(Action::Pending("data.recentSources"))), // pending; Excel: Recent Sources; gap #12; reserved until the command lands + ("APS", Entry::Finish(Action::Command("data.queriesConnections"))), // Excel: Show Queries + ("APT", Entry::Finish(Action::Pending("data.fromTable"))), // pending; Excel: From Table (query); gap #12; reserved until the command lands + ("AQ", Entry::Finish(Action::Command("data.advancedFilter"))), // Excel: Advanced Filter + ("AR", Entry::Continue), // group: group: Queries & Connections + ("ARA", Entry::Finish(Action::Command("data.refreshAll"))), // Excel: Refresh All + ("ARC", Entry::Finish(Action::Pending("data.cancelRefresh"))), // pending; Excel: Cancel Refresh; gap #12; reserved until the command lands + ("ARO", Entry::Finish(Action::Pending("data.connectionProperties"))), // pending; Excel: Connection Properties; gap #12; reserved until the command lands + ("ARR", Entry::Finish(Action::Command("pivot.refresh"))), // Excel: Refresh + ("ARS", Entry::Finish(Action::Pending("data.refreshStatus"))), // pending; Excel: Refresh Status; gap #12; reserved until the command lands + ("ASA", Entry::Finish(Action::Command("data.sortAscending"))), // Excel: Sort A to Z + ("ASD", Entry::Finish(Action::Command("data.sortDescending"))), // Excel: Sort Z to A + ("ASS", Entry::Finish(Action::Command("data.sort"))), // Excel: Sort + ("AT", Entry::Finish(Action::Command("data.filter"))), // Excel: Filter + ("AU", Entry::Continue), // Excel: Ungroup Menu; group level: containment(Ungroup) + ("AUC", Entry::Finish(Action::Pending("data.clearOutline"))), // pending; Excel: Clear Outline; feature gap; reserved until the command lands + ("AUU", Entry::Finish(Action::Command("data.ungroup"))), // Excel: Ungroup + ("AV", Entry::Continue), // Excel: Data Validation Menu; group level: containment(Data Validation) + ("AVI", Entry::Finish(Action::Command("data.circleInvalid"))), // Excel: Circle Invalid Data + ("AVR", Entry::Finish(Action::Pending("data.clearValidationCircles"))), // pending; Excel: Clear Validation Circles; reserved until the command lands + ("AVV", Entry::Finish(Action::Command("data.validation"))), // Excel: Data Validation + ("AW", Entry::Continue), // group: group: What-If (goalSeek/scenarioManager/dataTable) + ("AWG", Entry::Finish(Action::Command("data.goalSeek"))), // Excel: Goal Seek + ("AWS", Entry::Finish(Action::Command("data.scenarioManager"))), // Excel: Scenario Manager + ("AWT", Entry::Finish(Action::Command("data.dataTable"))), // Excel: Data Table + ("AX", Entry::Finish(Action::Pending("data.existingConnections"))), // pending; Excel: Existing Connections; gap #12; reserved until the command lands + ("AY", Entry::Finish(Action::Command("data.reapply"))), // Excel: Reapply Filter; containment(Reapply) + ("RA1", Entry::Finish(Action::Command("review.checkAccessibility"))), // Excel: Check Accessibility + ("RA2", Entry::Finish(Action::Pending("review.showComments"))), // pending; Excel: Show All Comments; Show All Comments; reserved until the command lands + ("RC", Entry::Finish(Action::Command("review.newComment"))), // Excel: New Comment + ("RD", Entry::Finish(Action::Command("review.deleteComment"))), // Excel: Delete Comment + ("RG", Entry::Finish(Action::Pending("review.showChanges"))), // pending; Excel: Track Changes; reserved until the command lands + ("RL", Entry::Finish(Action::Pending("`review.translate`"))), // pending; Excel: Translate; feature gap; reserved until the command lands + ("RN", Entry::Finish(Action::Pending("review.nextComment"))), // pending; Excel: Next Comment; Next Comment; reserved until the command lands + ("RO", Entry::Finish(Action::Command("review.protectWorkbook"))), // Excel: Protect and Share Workbook + ("RPS", Entry::Finish(Action::Command("review.protectSheet"))), // Excel: Protect Sheet + ("RPW", Entry::Finish(Action::Command("review.protectWorkbook"))), // Excel: Protect Workbook + ("RRS", Entry::Finish(Action::Pending("`review.smartLookup`"))), // pending; Excel: Smart Lookup; feature gap; reserved until the command lands + ("RS", Entry::Finish(Action::Command("review.spelling"))), // Excel: Spelling + ("RTN", Entry::Finish(Action::Command("review.newNote"))), // Excel: New Note + ("RV", Entry::Finish(Action::Pending("review.previousComment"))), // pending; Excel: Previous Comment; Previous Comment; reserved until the command lands + ("RW", Entry::Finish(Action::Pending("review.shareWorkbook"))), // pending; Excel: Share Workbook; reserved until the command lands + ("WA", Entry::Finish(Action::Command("view.arrangeAll"))), // Excel: Arrange All + ("WB", Entry::Finish(Action::Pending("view.sideBySide"))), // pending; Excel: View Side by Side; reserved until the command lands + ("WC", Entry::Finish(Action::Command("view.customViews"))), // Excel: Custom Views + ("WF", Entry::Continue), // Excel: Freeze Panes Menu; group level: + ("WFC", Entry::Finish(Action::Command("view.freezeFirstColumn"))), // Excel: Freeze First Column + ("WFF", Entry::Finish(Action::Command("view.freezePanes"))), // Excel: Freeze Panes + ("WFR", Entry::Finish(Action::Command("view.freezeTopRow"))), // Excel: Freeze Top Row + ("WG", Entry::Finish(Action::Command("view.zoomToSelection"))), // Excel: Zoom To Selection + ("WH", Entry::Finish(Action::Command("view.hideWindow"))), // Excel: Hide Window + ("WI", Entry::Finish(Action::Command("view.pageBreakPreview"))), // Excel: Page Break Preview + ("WJ", Entry::Finish(Action::Command("view.zoom"))), // Excel: Zoom To 100% + ("WL", Entry::Finish(Action::Command("view.normal"))), // Excel: Normal View; containment(Normal) + ("WM", Entry::Continue), // Excel: Macros Menu; group level: containment(Macros) + ("WMR", Entry::Finish(Action::Command("view.macros"))), // Excel: Record Macro; Record Macro (macros menu) + ("WMV", Entry::Finish(Action::Command("view.macros"))), // Excel: View Macros; containment(Macros) + ("WN", Entry::Finish(Action::Command("view.newWindow"))), // Excel: New Window + ("WP", Entry::Finish(Action::Command("view.pageLayout"))), // Excel: Page Layout View + ("WQ", Entry::Finish(Action::Command("view.zoom"))), // Excel: Zoom + ("WR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler + ("WS", Entry::Finish(Action::Command("view.split"))), // Excel: Split + ("WT", Entry::Finish(Action::Pending("view.resetWindowPosition"))), // pending; Excel: Reset Window Position; reserved until the command lands + ("WU", Entry::Finish(Action::Command("view.unhideWindow"))), // Excel: Unhide Window + ("WVC", Entry::Finish(Action::Pending("view.focusCell"))), // pending; Excel: Focus Cell Menu; Focus Cell; reserved until the command lands + ("WVF", Entry::Finish(Action::Pending("view.formulaBar"))), // pending; Excel: Toggle Formula Bar; Toggle Formula Bar; reserved until the command lands + ("WVH", Entry::Finish(Action::Command("view.headings"))), // Excel: Toggle Headings; containment(Headings) + ("WVL", Entry::Finish(Action::Pending("view.synchronousScrolling"))), // pending; Excel: Synchronous Scrolling; Synchronous Scrolling; reserved until the command lands + ("WVN", Entry::Finish(Action::Pending("view.navigation"))), // pending; Excel: Navigation; Navigation pane; reserved until the command lands + ("WVR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler + ("WVV", Entry::Finish(Action::Pending("view.sideBySide"))), // pending; Excel: View Side by Side; reserved until the command lands + ("WW", Entry::Finish(Action::Command("view.switchWindows"))), // Excel: Switch Windows + ("JAM", Entry::Finish(Action::Command("chart.formatSelection"))), // Excel: Chart format: Format Selection pane; containment(Format Selection) + ("JCA", Entry::Finish(Action::Command("chart.addElement"))), // Excel: Chart: Add Chart Element; containment(Add Chart Element) + ("JCC", Entry::Finish(Action::Command("chart.changeType"))), // Excel: Chart: Change Chart Type; containment(Change Chart Type) + ("JTS", Entry::Finish(Action::Command("table.style"))), // Excel: Table: table style + ("JTV", Entry::Finish(Action::Command("table.summarizePivot"))), // Excel: Table: Summarize with PivotTable; containment(Summarize with PivotTable) + ("FA", Entry::Finish(Action::Command("file.saveAs"))), // Excel: Save / Save As page; fires immediately in Excel; deeper aliases dropped as unreachable + ("FC", Entry::Finish(Action::Command("file.close"))), // Excel: Close workbook + ("FD", Entry::Finish(Action::Pending("file.account"))), // pending; Excel: Account page; Account page; reserved until the command lands + ("FEA", Entry::Finish(Action::Command("file.exportPdf"))), // Excel: Export: create PDF/XPS; Export: create PDF/XPS + ("FEC", Entry::Finish(Action::Command("file.saveAs"))), // Excel: Export: change file type; Export: change file type + ("FH", Entry::Finish(Action::Pending("file.home"))), // pending; Excel: File Home page (recent/pinned/shared/new blank); File Home page; reserved until the command lands + ("FI", Entry::Finish(Action::Command("file.properties"))), // Excel: Info page; fires immediately in Excel; deeper aliases dropped as unreachable + ("FN", Entry::Finish(Action::Command("file.new"))), // Excel: New page; New page + ("FO", Entry::Finish(Action::Command("file.open"))), // Excel: Open page; fires immediately in Excel; deeper aliases dropped as unreachable + ("FP", Entry::Finish(Action::Command("file.print"))), // Excel: Print page; containment(Print) + ("FT", Entry::Finish(Action::Pending("file.options"))), // pending; Excel: Options dialog; Options dialog; reserved until the command lands + ("Q", Entry::Finish(Action::Pending("ui.search"))), // pending; Excel: Search / Tell Me; Alt+Q search box does not exist yet (issue #49) + ("6", Entry::Finish(Action::Pending("qat.6"))), // pending; Excel: Quick Access Toolbar position 6; QAT navigation does not exist yet + ("7", Entry::Finish(Action::Pending("qat.7"))), // pending; Excel: Quick Access Toolbar position 7; QAT navigation does not exist yet + ("8", Entry::Finish(Action::Pending("qat.8"))), // pending; Excel: Quick Access Toolbar position 8; QAT navigation does not exist yet + ("9", Entry::Finish(Action::Pending("qat.9"))), // pending; Excel: Quick Access Toolbar position 9; QAT navigation does not exist yet ]; diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index 13489f1..1056d31 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -11,25 +11,2846 @@ " - Keep entries grouped by tab and sorted by sequence within a group; the FIRST entry for a tab is its canonical badge (legacy aliases come after it)." ], "sequences": [ - { "seq": "H", "kind": "tab", "target": "Home" }, - { "seq": "N", "kind": "tab", "target": "Insert" }, - { "seq": "JI", "kind": "tab", "target": "Draw", "note": "Two-letter unit. Deeper Draw letters differ between Excel and GridCraft; left blank for future contributors." }, - { "seq": "P", "kind": "tab", "target": "Page Layout" }, - { "seq": "M", "kind": "tab", "target": "Formulas" }, - { "seq": "A", "kind": "tab", "target": "Data" }, - { "seq": "R", "kind": "tab", "target": "Review" }, - { "seq": "W", "kind": "tab", "target": "View" }, - { "seq": "E", "kind": "tab", "target": "Home", "note": "Legacy Edit-menu path (Excel 2003); leads to the same Home menus." }, - { "seq": "O", "kind": "tab", "target": "Home", "note": "Legacy Format-menu path (Excel 2003); leads to the same Home menus." }, - - { "seq": "HAR", "kind": "command", "target": "home.alignRight" }, - { "seq": "HAL", "kind": "command", "target": "home.alignLeft" }, - { "seq": "HAC", "kind": "command", "target": "home.alignCenter" }, - { "seq": "HVS", "kind": "dialog", "target": "pasteSpecial" }, - { "seq": "ES", "kind": "dialog", "target": "pasteSpecial", "note": "Legacy Edit menu." }, - { "seq": "HOI", "kind": "command", "target": "home.autofitColumnWidth" }, - { "seq": "OCA", "kind": "command", "target": "home.autofitColumnWidth", "note": "Legacy Format menu." }, - - { "seq": "WVG", "kind": "command", "target": "view.gridlines" } + { + "seq": "H", + "kind": "tab", + "target": "Home" + }, + { + "seq": "N", + "kind": "tab", + "target": "Insert" + }, + { + "seq": "JI", + "kind": "tab", + "target": "Draw", + "note": "Two-letter unit. Deeper Draw letters differ between Excel and GridCraft; left blank for future contributors." + }, + { + "seq": "P", + "kind": "tab", + "target": "Page Layout" + }, + { + "seq": "M", + "kind": "tab", + "target": "Formulas" + }, + { + "seq": "A", + "kind": "tab", + "target": "Data" + }, + { + "seq": "R", + "kind": "tab", + "target": "Review" + }, + { + "seq": "W", + "kind": "tab", + "target": "View" + }, + { + "seq": "E", + "kind": "tab", + "target": "Home", + "note": "Legacy Edit-menu path (Excel 2003); leads to the same Home menus." + }, + { + "seq": "O", + "kind": "tab", + "target": "Home", + "note": "Legacy Format-menu path (Excel 2003); leads to the same Home menus." + }, + { + "seq": "HAR", + "kind": "command", + "target": "home.alignRight" + }, + { + "seq": "HAL", + "kind": "command", + "target": "home.alignLeft" + }, + { + "seq": "HAC", + "kind": "command", + "target": "home.alignCenter" + }, + { + "seq": "HVS", + "kind": "dialog", + "target": "pasteSpecial" + }, + { + "seq": "ES", + "kind": "dialog", + "target": "pasteSpecial", + "excel": "Classic Paste Special dialog (legacy Edit route); A/F/T/V are dialog mnemonics" + }, + { + "seq": "HOI", + "kind": "command", + "target": "home.autofitColumnWidth" + }, + { + "seq": "OCA", + "kind": "command", + "target": "home.autofitColumnWidth", + "note": "Legacy Format menu." + }, + { + "seq": "WVG", + "kind": "command", + "target": "view.gridlines" + }, + { + "seq": "0", + "kind": "command", + "target": "qat.10", + "pending": true, + "excel": "Quick Access Toolbar position 10", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "1", + "kind": "command", + "target": "qat.1", + "pending": true, + "excel": "Quick Access Toolbar position 1", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "2", + "kind": "command", + "target": "qat.2", + "pending": true, + "excel": "Quick Access Toolbar position 2", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "3", + "kind": "command", + "target": "qat.3", + "pending": true, + "excel": "Quick Access Toolbar position 3", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "4", + "kind": "command", + "target": "qat.4", + "pending": true, + "excel": "Quick Access Toolbar position 4", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "5", + "kind": "command", + "target": "qat.5", + "pending": true, + "excel": "Quick Access Toolbar position 5", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "H0", + "kind": "command", + "target": "home.increaseDecimal", + "excel": "Increase Decimals", + "note": "containment(Increase Decimal)" + }, + { + "seq": "H1", + "kind": "command", + "target": "home.bold", + "excel": "Bold", + "note": "" + }, + { + "seq": "H2", + "kind": "command", + "target": "home.italic", + "excel": "Italic", + "note": "" + }, + { + "seq": "H3", + "kind": "continue", + "excel": "Underline Menu", + "note": "group level:" + }, + { + "seq": "H3D", + "kind": "command", + "target": "home.doubleUnderline", + "excel": "Double Underline", + "note": "" + }, + { + "seq": "H3U", + "kind": "command", + "target": "home.underline", + "excel": "Underline", + "note": "" + }, + { + "seq": "H5", + "kind": "command", + "target": "home.decreaseIndent", + "excel": "Decrease Indent", + "note": "" + }, + { + "seq": "H6", + "kind": "command", + "target": "home.increaseIndent", + "excel": "Increase Indent", + "note": "" + }, + { + "seq": "H9", + "kind": "command", + "target": "home.decreaseDecimal", + "excel": "Decrease Decimals", + "note": "containment(Decrease Decimal)" + }, + { + "seq": "HAN", + "kind": "command", + "excel": "Accounting Format", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "home.accounting" + }, + { + "seq": "HAS", + "kind": "command", + "target": "home.addIns", + "excel": "Add-ins", + "note": "containment(Add-ins)" + }, + { + "seq": "HB", + "kind": "continue", + "excel": "Borders Menu", + "note": "group level:" + }, + { + "seq": "HBA", + "kind": "command", + "target": "home.borders", + "excel": "All Borders", + "note": "containment(Borders)" + }, + { + "seq": "HBB", + "kind": "command", + "target": "home.borders", + "excel": "Bottom Double Border", + "note": "border-side TBD" + }, + { + "seq": "HBC", + "kind": "command", + "target": "home.borders", + "excel": "Top and Thick Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBD", + "kind": "command", + "target": "home.borders", + "excel": "Top and Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBE", + "kind": "command", + "target": "home.borders", + "excel": "Erase Border", + "note": "border-side TBD" + }, + { + "seq": "HBG", + "kind": "command", + "target": "home.drawBorder", + "excel": "Draw Border Grid", + "note": "containment(Draw Border)" + }, + { + "seq": "HBH", + "kind": "command", + "target": "home.borders", + "excel": "Thick Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBI", + "kind": "command", + "target": "home.borders", + "excel": "Line Color", + "note": "Line Color (submenu TBD)" + }, + { + "seq": "HBL", + "kind": "command", + "target": "home.borders", + "excel": "Left Border", + "note": "border-side TBD" + }, + { + "seq": "HBM", + "kind": "command", + "target": "home.borders", + "excel": "More Borders", + "note": "containment(Borders)" + }, + { + "seq": "HBN", + "kind": "command", + "target": "home.borders", + "excel": "No Border", + "note": "border-side TBD" + }, + { + "seq": "HBO", + "kind": "command", + "target": "home.borders", + "excel": "Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBP", + "kind": "command", + "target": "home.borders", + "excel": "Top Border", + "note": "border-side TBD" + }, + { + "seq": "HBR", + "kind": "command", + "target": "home.borders", + "excel": "Right Border", + "note": "border-side TBD" + }, + { + "seq": "HBS", + "kind": "command", + "target": "home.borders", + "excel": "Outside Borders", + "note": "containment(Borders)" + }, + { + "seq": "HBT", + "kind": "command", + "target": "home.borders", + "excel": "Thick Outside Borders", + "note": "border-side TBD" + }, + { + "seq": "HBU", + "kind": "command", + "target": "home.borders", + "excel": "Top and Double Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBW", + "kind": "command", + "target": "home.drawBorder", + "excel": "Draw Border", + "note": "" + }, + { + "seq": "HBY", + "kind": "command", + "target": "home.borders", + "excel": "Line Style", + "note": "Line Style (submenu TBD)" + }, + { + "seq": "HC", + "kind": "command", + "excel": "Copy", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "edit.copy" + }, + { + "seq": "HCP", + "kind": "command", + "target": "home.copyAsPicture", + "pending": true, + "excel": "Copy as Picture", + "note": "reserved until the command lands" + }, + { + "seq": "HDC", + "kind": "command", + "target": "home.deleteColumns", + "excel": "Delete Columns", + "note": "Delete Columns" + }, + { + "seq": "HDD", + "kind": "command", + "target": "home.deleteCells", + "excel": "Delete Cells", + "note": "" + }, + { + "seq": "HDR", + "kind": "command", + "target": "home.deleteRows", + "excel": "Delete Rows", + "note": "Delete Rows" + }, + { + "seq": "HDS", + "kind": "command", + "target": "home.deleteSheet", + "excel": "Delete Sheet", + "note": "" + }, + { + "seq": "HEA", + "kind": "command", + "target": "edit.clearAll", + "excel": "Clear All", + "note": "" + }, + { + "seq": "HEC", + "kind": "command", + "target": "edit.clearContents", + "excel": "Clear Contents", + "note": "" + }, + { + "seq": "HEF", + "kind": "command", + "target": "edit.clearFormats", + "excel": "Clear Formats", + "note": "" + }, + { + "seq": "HEL", + "kind": "command", + "target": "edit.clearHyperlinks", + "excel": "Clear Hyperlinks", + "note": "" + }, + { + "seq": "HEM", + "kind": "command", + "target": "edit.clearComments", + "excel": "Clear Comments and Notes", + "note": "containment(Clear Comments and Notes)" + }, + { + "seq": "HFC", + "kind": "command", + "target": "home.fontColor", + "excel": "Font Color", + "note": "" + }, + { + "seq": "HFDC", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "Find Conditional Formatting", + "note": "containment(Conditional Formatting)" + }, + { + "seq": "HFDF", + "kind": "command", + "target": "edit.find", + "excel": "Find", + "note": "" + }, + { + "seq": "HFDG", + "kind": "command", + "target": "edit.goTo", + "excel": "Go To", + "note": "" + }, + { + "seq": "HFDM", + "kind": "command", + "target": "edit.find", + "excel": "Find Notes", + "note": "containment(Find)" + }, + { + "seq": "HFDO", + "kind": "command", + "target": "home.selectObjects", + "pending": true, + "excel": "Select Objects", + "note": "Select Objects; reserved until the command lands" + }, + { + "seq": "HFDP", + "kind": "command", + "target": "arrange.selectionPane", + "excel": "Selection Pane", + "note": "" + }, + { + "seq": "HFDR", + "kind": "command", + "target": "edit.replace", + "excel": "Replace", + "note": "" + }, + { + "seq": "HFDS", + "kind": "command", + "target": "edit.goToSpecial", + "excel": "Go To Special", + "note": "" + }, + { + "seq": "HFF", + "kind": "command", + "target": "home.fontName", + "excel": "Font Selection", + "note": "" + }, + { + "seq": "HFG", + "kind": "command", + "target": "home.increaseFontSize", + "excel": "Increase Font Size", + "note": "" + }, + { + "seq": "HFI", + "kind": "continue", + "excel": "Fill", + "note": "group level: containment(Fill Up)" + }, + { + "seq": "HFIA", + "kind": "command", + "target": "home.fillAcrossWorksheets", + "pending": true, + "excel": "Fill Across Worksheets", + "note": "Fill Across Worksheets; reserved until the command lands" + }, + { + "seq": "HFID", + "kind": "command", + "target": "edit.fillDown", + "excel": "Fill Down", + "note": "" + }, + { + "seq": "HFIF", + "kind": "command", + "target": "edit.flashFill", + "excel": "Flash Fill", + "note": "" + }, + { + "seq": "HFIJ", + "kind": "command", + "target": "home.justify", + "excel": "Fill Justify", + "note": "containment(Justify)" + }, + { + "seq": "HFIL", + "kind": "command", + "target": "edit.fillLeft", + "excel": "Fill Left", + "note": "" + }, + { + "seq": "HFIR", + "kind": "command", + "target": "edit.fillRight", + "excel": "Fill Right", + "note": "" + }, + { + "seq": "HFIS", + "kind": "command", + "target": "edit.fillSeries", + "excel": "Fill Series", + "note": "containment(Series)" + }, + { + "seq": "HFIU", + "kind": "command", + "target": "edit.fillUp", + "excel": "Fill Up", + "note": "" + }, + { + "seq": "HFK", + "kind": "command", + "target": "home.decreaseFontSize", + "excel": "Decrease Font Size", + "note": "" + }, + { + "seq": "HFM", + "kind": "command", + "target": "home.numberFormat", + "excel": "More Number Formats", + "note": "containment(Number Format)" + }, + { + "seq": "HFP", + "kind": "command", + "target": "edit.formatPainter", + "excel": "Format Painter", + "note": "" + }, + { + "seq": "HFQ", + "kind": "continue", + "excel": "Orientation Menu", + "note": "group level: containment(Orientation)" + }, + { + "seq": "HFQL", + "kind": "command", + "target": "home.angleClockwise", + "pending": true, + "excel": "Angle Clockwise", + "note": "Angle Clockwise; reserved until the command lands" + }, + { + "seq": "HFQO", + "kind": "command", + "target": "home.angleCounterclockwise", + "pending": true, + "excel": "Angle Counterclockwise", + "note": "Angle Counterclockwise; reserved until the command lands" + }, + { + "seq": "HFS", + "kind": "command", + "target": "home.fontSize", + "excel": "Font Size", + "note": "" + }, + { + "seq": "HH", + "kind": "command", + "target": "home.fillColor", + "excel": "Fill Color", + "note": "" + }, + { + "seq": "HIC", + "kind": "command", + "target": "home.insertColumns", + "excel": "Insert Columns", + "note": "Insert Columns" + }, + { + "seq": "HIE", + "kind": "command", + "target": "home.insertCells", + "excel": "Insert Copied Cells", + "note": "Insert Cells (copied cells TBD)" + }, + { + "seq": "HII", + "kind": "command", + "target": "home.insertCells", + "excel": "Insert Cells", + "note": "" + }, + { + "seq": "HIR", + "kind": "command", + "target": "home.insertRows", + "excel": "Insert Rows", + "note": "Insert Rows" + }, + { + "seq": "HIS", + "kind": "command", + "target": "home.insertSheet", + "excel": "Insert Sheet", + "note": "" + }, + { + "seq": "HJ", + "kind": "continue", + "excel": "Cell Styles", + "note": "group level:" + }, + { + "seq": "HJN", + "kind": "command", + "target": "home.newCellStyle", + "excel": "New Cell Style", + "note": "" + }, + { + "seq": "HK", + "kind": "command", + "target": "home.comma", + "excel": "Comma Style", + "note": "" + }, + { + "seq": "HL", + "kind": "continue", + "excel": "Conditional Formatting", + "note": "group level: containment(Conditional Formatting)" + }, + { + "seq": "HLC", + "kind": "continue", + "excel": "Clear Rules Menu", + "note": "group level: containment(Clear Rules)" + }, + { + "seq": "HLCE", + "kind": "command", + "target": "home.clearRules", + "excel": "Clear Rules From Sheet", + "note": "containment(Clear Rules)" + }, + { + "seq": "HLCS", + "kind": "command", + "target": "home.clearRules", + "excel": "Clear Rules From Cells", + "note": "containment(Clear Rules)" + }, + { + "seq": "HLCT", + "kind": "command", + "target": "home.clearRules", + "excel": "Clear Rules From Table", + "note": "containment(Clear Rules)" + }, + { + "seq": "HLD", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "Data Bars", + "note": "Data Bars (submenu TBD)" + }, + { + "seq": "HLH", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "Highlight Cells", + "note": "Highlight Cells (submenu TBD)" + }, + { + "seq": "HLN", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "New Rule", + "note": "New Rule (submenu TBD)" + }, + { + "seq": "HLR", + "kind": "command", + "target": "home.manageRules", + "excel": "Manage Rules", + "note": "containment(Manage Rules)" + }, + { + "seq": "HLT", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "Top/Bottom Rules", + "note": "Top/Bottom (submenu TBD)" + }, + { + "seq": "HM", + "kind": "continue", + "excel": "Merge & Center Menu", + "note": "group level: containment(Merge & Center)" + }, + { + "seq": "HMA", + "kind": "command", + "target": "home.mergeAcross", + "excel": "Merge Across", + "note": "" + }, + { + "seq": "HMC", + "kind": "command", + "target": "home.mergeCenter", + "excel": "Merge & Center", + "note": "" + }, + { + "seq": "HMM", + "kind": "command", + "target": "home.mergeCells", + "excel": "Merge Cells", + "note": "" + }, + { + "seq": "HMU", + "kind": "command", + "target": "home.unmergeCells", + "excel": "Unmerge Cells", + "note": "" + }, + { + "seq": "HN", + "kind": "command", + "target": "home.numberFormat", + "excel": "Number Format", + "note": "" + }, + { + "seq": "HOA", + "kind": "command", + "target": "home.autofitRowHeight", + "excel": "AutoFit Row Height", + "note": "" + }, + { + "seq": "HOD", + "kind": "command", + "target": "home.columnWidth", + "excel": "Default Column Width", + "note": "containment(Column Width)" + }, + { + "seq": "HOE", + "kind": "command", + "target": "home.formatCells", + "excel": "Format Cells", + "note": "" + }, + { + "seq": "HOH", + "kind": "command", + "target": "home.rowHeight", + "excel": "Row Height", + "note": "" + }, + { + "seq": "HOL", + "kind": "command", + "target": "home.lockCell", + "excel": "Lock Cell", + "note": "containment(Lock Cell)" + }, + { + "seq": "HOM", + "kind": "command", + "target": "sheet.move", + "excel": "Move or Copy Sheet", + "note": "" + }, + { + "seq": "HOP", + "kind": "command", + "target": "review.protectSheet", + "excel": "Protect Sheet", + "note": "" + }, + { + "seq": "HOR", + "kind": "command", + "target": "sheet.rename", + "excel": "Rename Sheet", + "note": "" + }, + { + "seq": "HOT", + "kind": "command", + "target": "sheet.tabColor", + "excel": "Tab Color", + "note": "" + }, + { + "seq": "HOUC", + "kind": "command", + "target": "home.hideColumns", + "excel": "Hide Columns", + "note": "" + }, + { + "seq": "HOUH", + "kind": "command", + "target": "sheet.unhide", + "excel": "Unhide Sheet", + "note": "" + }, + { + "seq": "HOUL", + "kind": "command", + "target": "home.unhideColumns", + "excel": "Unhide Columns", + "note": "" + }, + { + "seq": "HOUO", + "kind": "command", + "target": "home.unhideRows", + "excel": "Unhide Rows", + "note": "" + }, + { + "seq": "HOUR", + "kind": "command", + "target": "home.hideRows", + "excel": "Hide Rows", + "note": "" + }, + { + "seq": "HOUS", + "kind": "command", + "target": "sheet.hide", + "excel": "Hide Sheet", + "note": "" + }, + { + "seq": "HOW", + "kind": "command", + "target": "home.columnWidth", + "excel": "Column Width", + "note": "" + }, + { + "seq": "HP", + "kind": "command", + "target": "home.numberFormat", + "excel": "Percentage Style", + "note": "format TBD" + }, + { + "seq": "HSC", + "kind": "command", + "target": "data.clearFilter", + "excel": "Clear Filter", + "note": "" + }, + { + "seq": "HSF", + "kind": "command", + "target": "data.filter", + "excel": "Filter", + "note": "" + }, + { + "seq": "HSO", + "kind": "command", + "target": "data.sortDescending", + "excel": "Sort Z to A", + "note": "" + }, + { + "seq": "HSS", + "kind": "command", + "target": "data.sortAscending", + "excel": "Sort A to Z", + "note": "" + }, + { + "seq": "HSU", + "kind": "command", + "target": "data.sort", + "excel": "Custom Sort", + "note": "" + }, + { + "seq": "HT", + "kind": "continue", + "excel": "Format as Table", + "note": "group level:" + }, + { + "seq": "HTN", + "kind": "command", + "target": "table.style", + "excel": "New Table Style", + "note": "" + }, + { + "seq": "HTP", + "kind": "command", + "target": "table.style", + "excel": "New Pivot Table Style", + "note": "" + }, + { + "seq": "HU", + "kind": "continue", + "excel": "AutoSum", + "note": "group level:" + }, + { + "seq": "HUA", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Auto Average", + "note": "AutoSum: Average (variant TBD)" + }, + { + "seq": "HUC", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Auto Count Numbers", + "note": "AutoSum: Count (variant TBD)" + }, + { + "seq": "HUI", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Auto Min", + "note": "AutoSum: Min (variant TBD)" + }, + { + "seq": "HUM", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Auto Max", + "note": "AutoSum: Max (variant TBD)" + }, + { + "seq": "HV", + "kind": "continue", + "excel": "Paste", + "note": "group level:" + }, + { + "seq": "HVA", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Values and Number Formatting", + "note": "paste variant, params TBD: what=TBD(values+numfmt)" + }, + { + "seq": "HVB", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste No Borders", + "note": "paste variant, params TBD: what=TBD(no-borders)" + }, + { + "seq": "HVE", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Values and Source Formatting", + "note": "paste variant, params TBD: what=TBD(values+src-fmt)" + }, + { + "seq": "HVF", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Formula", + "note": "paste-variant TBD" + }, + { + "seq": "HVI", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Linked Picture", + "note": "what=TBD(linked-picture)" + }, + { + "seq": "HVK", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Keep Source Formatting", + "note": "paste variant, params TBD: what=TBD(keep-source)" + }, + { + "seq": "HVM", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Destination Formatting", + "note": "paste variant, params TBD: what=TBD(dest)" + }, + { + "seq": "HVN", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Link", + "note": "paste variant, params TBD: what=TBD(link)" + }, + { + "seq": "HVO", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Formula and Formatting", + "note": "paste variant, params TBD: what=TBD(formula+formatting)" + }, + { + "seq": "HVP", + "kind": "command", + "target": "edit.paste", + "excel": "Paste", + "note": "" + }, + { + "seq": "HVR", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Formatting", + "note": "paste variant, params TBD: what=formats" + }, + { + "seq": "HVT", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Transpose", + "note": "paste variant, params TBD: what=transpose" + }, + { + "seq": "HVU", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Picture", + "note": "what=TBD(picture)" + }, + { + "seq": "HVV", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Values", + "note": "paste variant, params TBD: what=values" + }, + { + "seq": "HVW", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Keep Source Column Widths", + "note": "paste variant, params TBD: what=TBD(col-widths)" + }, + { + "seq": "HW", + "kind": "continue", + "excel": "Wrap Text Menu", + "note": "group level: containment(Wrap Text)" + }, + { + "seq": "HWS", + "kind": "command", + "target": "home.shrinkToFit", + "pending": true, + "excel": "Shrink to Fit", + "note": "Shrink to Fit; reserved until the command lands" + }, + { + "seq": "HWW", + "kind": "command", + "target": "home.wrapText", + "excel": "Wrap Text", + "note": "" + }, + { + "seq": "HX", + "kind": "command", + "target": "edit.cut", + "excel": "Cut", + "note": "" + }, + { + "seq": "NC1", + "kind": "command", + "target": "insert.chart", + "excel": "Column Chart", + "note": "Column chart (kind TBD)" + }, + { + "seq": "NC2", + "kind": "command", + "target": "review.newComment", + "excel": "New Comment", + "note": "" + }, + { + "seq": "NCB", + "kind": "command", + "target": "insert.checkbox", + "excel": "Checkbox", + "note": "" + }, + { + "seq": "ND", + "kind": "command", + "target": "insert.chart", + "excel": "Scatter Chart", + "note": "Scatter (kind TBD)" + }, + { + "seq": "NE", + "kind": "command", + "target": "insert.equation", + "pending": true, + "excel": "Equations", + "note": "Equations; reserved until the command lands" + }, + { + "seq": "NF", + "kind": "continue", + "excel": "Online Pictures", + "note": "group level: containment(Picture)" + }, + { + "seq": "NFP", + "kind": "continue", + "excel": "From Picture Menu", + "note": "group level: containment(Picture)" + }, + { + "seq": "NFPF", + "kind": "command", + "target": "insert.picture", + "excel": "Picture From File", + "note": "containment(Picture)" + }, + { + "seq": "NH1", + "kind": "command", + "target": "pageLayout.headerFooter", + "excel": "Header & Footer", + "note": "" + }, + { + "seq": "NHF", + "kind": "command", + "target": "pageLayout.headerFooter", + "excel": "Header & Footer", + "note": "" + }, + { + "seq": "NHI", + "kind": "command", + "target": "insert.chart", + "excel": "Hierarchy Chart", + "note": "Hierarchy (kind TBD)" + }, + { + "seq": "NI1", + "kind": "command", + "target": "insert.chart", + "excel": "Waterfall Chart", + "note": "Waterfall (kind TBD)" + }, + { + "seq": "NI2", + "kind": "command", + "target": "insert.link", + "excel": "Link", + "note": "" + }, + { + "seq": "NJ", + "kind": "command", + "target": "`insert.object`", + "pending": true, + "excel": "Object", + "note": "insert.object missing; reserved until the command lands" + }, + { + "seq": "NM1T", + "kind": "command", + "target": "insert.picture", + "excel": "SmartArt Picture", + "note": "containment(Picture)" + }, + { + "seq": "NM2", + "kind": "command", + "target": "insert.chart", + "excel": "Map Chart", + "note": "Map (kind TBD)" + }, + { + "seq": "NN1", + "kind": "command", + "target": "insert.chart", + "excel": "Line Chart", + "note": "Line chart (kind TBD)" + }, + { + "seq": "NNS", + "kind": "command", + "target": "insert.icons", + "excel": "Icons", + "note": "" + }, + { + "seq": "NP", + "kind": "continue", + "excel": "Picture", + "note": "group level:" + }, + { + "seq": "NPB", + "kind": "command", + "target": "insert.picture", + "excel": "Picture from File", + "note": "containment(Picture)" + }, + { + "seq": "NPF", + "kind": "command", + "target": "insert.picture", + "excel": "Place in Cell", + "note": "Picture in cell (source TBD)" + }, + { + "seq": "NPI", + "kind": "command", + "target": "insert.picture", + "excel": "Place in Cell Menu", + "note": "Picture in cell (source TBD)" + }, + { + "seq": "NPO", + "kind": "command", + "target": "insert.picture", + "excel": "Place over Cells Menu", + "note": "Picture over cells (source TBD)" + }, + { + "seq": "NQ", + "kind": "command", + "target": "insert.chart", + "excel": "Pie Chart", + "note": "Pie (kind TBD)" + }, + { + "seq": "NR", + "kind": "command", + "target": "insert.recommendedCharts", + "excel": "Recommended Charts", + "note": "" + }, + { + "seq": "NSA", + "kind": "command", + "target": "insert.chart", + "excel": "Statistical Chart", + "note": "Statistical chart (kind TBD)" + }, + { + "seq": "NSC", + "kind": "command", + "target": "insert.screenshot", + "pending": true, + "excel": "Screenshot Menu", + "note": "Screenshot; reserved until the command lands" + }, + { + "seq": "NSD", + "kind": "command", + "target": "insert.chart", + "excel": "Combo Chart", + "note": "Combo (kind TBD)" + }, + { + "seq": "NSF", + "kind": "command", + "target": "`insert.slicer`", + "pending": true, + "excel": "Slicer", + "note": "insert.slicer missing (gap #8); reserved until the command lands" + }, + { + "seq": "NSH", + "kind": "command", + "target": "insert.shape", + "excel": "Shapes", + "note": "" + }, + { + "seq": "NSP", + "kind": "command", + "target": "insert.recommendedPivotTables", + "excel": "Recommended PivotTables", + "note": "" + }, + { + "seq": "NST", + "kind": "command", + "target": "`insert.timeline`", + "pending": true, + "excel": "Timeline", + "note": "insert.timeline missing (gap #8); reserved until the command lands" + }, + { + "seq": "NSZ", + "kind": "command", + "target": "insert.pivotChart", + "pending": true, + "excel": "PivotChart", + "note": "PivotChart; reserved until the command lands" + }, + { + "seq": "NT", + "kind": "command", + "target": "insert.table", + "excel": "Table", + "note": "" + }, + { + "seq": "NU", + "kind": "command", + "target": "insert.symbol", + "excel": "Symbols", + "note": "containment(Symbol)" + }, + { + "seq": "NV", + "kind": "command", + "target": "insert.pivotTable", + "excel": "PivotTable", + "note": "" + }, + { + "seq": "NW", + "kind": "command", + "target": "insert.wordArt", + "pending": true, + "excel": "WordArt", + "note": "WordArt; reserved until the command lands" + }, + { + "seq": "NX", + "kind": "command", + "target": "insert.textBox", + "excel": "Text Box", + "note": "" + }, + { + "seq": "NZK", + "kind": "continue", + "excel": "Sparklines Menu", + "note": "group level:" + }, + { + "seq": "NZKSL", + "kind": "command", + "target": "insert.sparkline", + "excel": "Line Sparkline", + "note": "" + }, + { + "seq": "NZKSO", + "kind": "command", + "target": "insert.sparkline", + "excel": "Column Sparkline", + "note": "" + }, + { + "seq": "NZKSW", + "kind": "command", + "target": "insert.sparkline", + "excel": "Win/Loss Sparkline", + "note": "" + }, + { + "seq": "NZTX", + "kind": "command", + "target": "insert.textBox", + "excel": "Text Box", + "note": "" + }, + { + "seq": "PAE", + "kind": "command", + "target": "arrange.sendBackward", + "excel": "Send Backward", + "note": "" + }, + { + "seq": "PAF", + "kind": "command", + "target": "arrange.bringForward", + "excel": "Bring Forward", + "note": "" + }, + { + "seq": "PB", + "kind": "continue", + "excel": "Breaks", + "note": "group level:" + }, + { + "seq": "PBI", + "kind": "command", + "target": "pageLayout.breaks", + "excel": "Insert Page Break", + "note": "Insert Page Break" + }, + { + "seq": "PBR", + "kind": "command", + "target": "pageLayout.breaks", + "excel": "Remove Page Break", + "note": "Remove Page Break" + }, + { + "seq": "PG", + "kind": "command", + "target": "pageLayout.background", + "excel": "Background", + "note": "" + }, + { + "seq": "PI", + "kind": "command", + "target": "pageLayout.printTitles", + "excel": "Print Titles", + "note": "" + }, + { + "seq": "PM", + "kind": "continue", + "excel": "Margins", + "note": "group level:" + }, + { + "seq": "PMA", + "kind": "command", + "target": "pageLayout.margins", + "excel": "Custom Margins", + "note": "containment(Margins)" + }, + { + "seq": "PO", + "kind": "command", + "target": "pageLayout.orientation", + "excel": "Orientation", + "note": "" + }, + { + "seq": "PPG", + "kind": "command", + "target": "pageLayout.printGridlines", + "excel": "Print Gridlines", + "note": "" + }, + { + "seq": "PPH", + "kind": "command", + "target": "pageLayout.printHeadings", + "excel": "Print Headings", + "note": "" + }, + { + "seq": "PR", + "kind": "continue", + "excel": "Print Area", + "note": "group level:" + }, + { + "seq": "PRS", + "kind": "command", + "target": "pageLayout.printArea", + "excel": "Set Print Area", + "note": "" + }, + { + "seq": "PSP", + "kind": "command", + "target": "pageLayout.pageSetup", + "excel": "Page Setup", + "note": "" + }, + { + "seq": "PSZ", + "kind": "command", + "target": "pageLayout.size", + "excel": "Size", + "note": "" + }, + { + "seq": "PTC", + "kind": "command", + "target": "pageLayout.themeColors", + "excel": "Theme Colors", + "note": "containment(Colors)" + }, + { + "seq": "PTE", + "kind": "command", + "target": "pageLayout.themeEffects", + "excel": "Theme Effects", + "note": "containment(Effects)" + }, + { + "seq": "PTF", + "kind": "command", + "target": "pageLayout.themeFonts", + "excel": "Theme Fonts", + "note": "containment(Fonts)" + }, + { + "seq": "PTH", + "kind": "continue", + "excel": "Themes", + "note": "group level:" + }, + { + "seq": "PTHB", + "kind": "command", + "target": "pageLayout.theme", + "excel": "Browse Themes", + "note": "containment(Themes)" + }, + { + "seq": "PVG", + "kind": "command", + "target": "view.gridlines", + "excel": "View Gridlines", + "note": "" + }, + { + "seq": "PVH", + "kind": "command", + "target": "view.headings", + "excel": "View Headings", + "note": "" + }, + { + "seq": "MA", + "kind": "continue", + "excel": "Remove Arrows Menu", + "note": "group level: containment(Remove Arrows)" + }, + { + "seq": "MAA", + "kind": "command", + "target": "formulas.removeArrows", + "pending": true, + "excel": "Remove All Arrows", + "note": "Remove All Arrows; reserved until the command lands" + }, + { + "seq": "MAP", + "kind": "command", + "target": "formulas.removePrecedentArrows", + "pending": true, + "excel": "Remove Precedent Arrows", + "note": "Remove Precedent Arrows; reserved until the command lands" + }, + { + "seq": "MB", + "kind": "command", + "target": "formulas.calculateNow", + "excel": "Calculate Now", + "note": "" + }, + { + "seq": "MC", + "kind": "command", + "target": "formulas.createFromSelection", + "excel": "Create From Selection", + "note": "" + }, + { + "seq": "MD", + "kind": "command", + "target": "formulas.traceDependents", + "excel": "Trace Dependents", + "note": "" + }, + { + "seq": "ME", + "kind": "continue", + "excel": "Date & Time Menu", + "note": "group level: containment(Date & Time)" + }, + { + "seq": "MEF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Date & Time - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Insert Function", + "note": "" + }, + { + "seq": "MG", + "kind": "continue", + "excel": "Math & Trig Menu", + "note": "group level: containment(Math & Trig)" + }, + { + "seq": "MGF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Math & Trig - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MH", + "kind": "command", + "target": "formulas.showFormulas", + "excel": "Show Formulas", + "note": "" + }, + { + "seq": "MI", + "kind": "continue", + "excel": "Financial Menu", + "note": "group level: containment(Financial)" + }, + { + "seq": "MIF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Financial - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MJ", + "kind": "command", + "target": "formulas.calculateSheet", + "excel": "Calculate Sheet", + "note": "" + }, + { + "seq": "MK", + "kind": "continue", + "excel": "Error Checking Menu", + "note": "group level: containment(Error Checking)" + }, + { + "seq": "MKK", + "kind": "command", + "target": "formulas.errorChecking", + "excel": "Error Checking", + "note": "" + }, + { + "seq": "ML", + "kind": "continue", + "excel": "Logical Menu", + "note": "group level: containment(Logical)" + }, + { + "seq": "MLF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Logical - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MM", + "kind": "command", + "target": "formulas.defineName", + "excel": "Define Name", + "note": "" + }, + { + "seq": "MN", + "kind": "command", + "target": "formulas.nameManager", + "excel": "Name Manager", + "note": "" + }, + { + "seq": "MO", + "kind": "continue", + "excel": "Lookup & Reference Menu", + "note": "group level: containment(Lookup & Reference)" + }, + { + "seq": "MOF", + "kind": "command", + "target": "formulas.lookupReference", + "excel": "Lookup & Reference - Insert Function", + "note": "containment(Lookup & Reference)" + }, + { + "seq": "MP", + "kind": "command", + "target": "formulas.tracePrecedents", + "excel": "Trace Precedents", + "note": "" + }, + { + "seq": "MQ", + "kind": "continue", + "excel": "More Functions Menu", + "note": "group level: containment(More Functions)" + }, + { + "seq": "MQCF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Cube - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MQEF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Engineering - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MQIF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Information - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MQOF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Compatibility - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MQSF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Statistical - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MQWF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Web - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MR", + "kind": "continue", + "excel": "Recently Used Menu", + "note": "group level: containment(Recently Used)" + }, + { + "seq": "MRF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Recently Used - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MS", + "kind": "continue", + "excel": "Use in Formula Menu", + "note": "group level: containment(Use in Formula)" + }, + { + "seq": "MSP", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste Names", + "note": "paste-variant TBD" + }, + { + "seq": "MT", + "kind": "continue", + "excel": "Text Menu", + "note": "group level: containment(Text)" + }, + { + "seq": "MTF", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Text - Insert Function", + "note": "containment(Insert Function)" + }, + { + "seq": "MU", + "kind": "continue", + "excel": "AutoSum Menu", + "note": "group level:" + }, + { + "seq": "MUA", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Average", + "note": "AutoSum: Average (variant TBD)" + }, + { + "seq": "MUC", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Count Numbers", + "note": "AutoSum: Count (variant TBD)" + }, + { + "seq": "MUF", + "kind": "command", + "target": "formulas.moreFunctions", + "excel": "More Functions (AutoSum)", + "note": "containment(More Functions)" + }, + { + "seq": "MUI", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Min", + "note": "AutoSum: Min (variant TBD)" + }, + { + "seq": "MUM", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Max", + "note": "AutoSum: Max (variant TBD)" + }, + { + "seq": "MUS", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Sum", + "note": "" + }, + { + "seq": "MV", + "kind": "command", + "target": "formulas.evaluateFormula", + "excel": "Evaluate Formula", + "note": "" + }, + { + "seq": "MW", + "kind": "command", + "target": "formulas.watchWindow", + "excel": "Watch Window", + "note": "" + }, + { + "seq": "MX", + "kind": "continue", + "excel": "Calculation Options Menu", + "note": "group level: containment(Calculation Options)" + }, + { + "seq": "MXA", + "kind": "command", + "target": "formulas.calculationOptions", + "excel": "Automatic Calculation", + "note": "Calculation options: Automatic" + }, + { + "seq": "MXM", + "kind": "command", + "target": "formulas.calculationOptions", + "excel": "Manual Calculation", + "note": "Calculation options: Manual" + }, + { + "seq": "MXP", + "kind": "command", + "target": "formulas.calculationOptions", + "excel": "Partial Calculation", + "note": "Calculation options: Partial" + }, + { + "seq": "AB", + "kind": "command", + "target": "data.subtotal", + "excel": "Subtotal", + "note": "" + }, + { + "seq": "AC", + "kind": "command", + "target": "data.clearFilter", + "excel": "Clear Filter", + "note": "" + }, + { + "seq": "AD", + "kind": "command", + "target": "data.consolidate", + "excel": "Consolidate", + "note": "" + }, + { + "seq": "AE", + "kind": "command", + "target": "data.textToColumns", + "excel": "Text to Columns", + "note": "" + }, + { + "seq": "AF1", + "kind": "continue", + "excel": "Data from Picture Menu", + "note": "group level: feature gap; reserved until the command lands" + }, + { + "seq": "AF1C", + "kind": "command", + "target": "insert.picture", + "excel": "Picture from Clipboard (source TBD)" + }, + { + "seq": "AF1P", + "kind": "command", + "target": "insert.picture", + "excel": "Picture from File (source TBD)" + }, + { + "seq": "AFA", + "kind": "command", + "target": "data.fromAccess", + "pending": true, + "excel": "Get External Data: From Access", + "note": "gap #12 Get & Transform; reserved until the command lands" + }, + { + "seq": "AFC", + "kind": "command", + "target": "`data.forecastSheet`", + "pending": true, + "excel": "Forecast Sheet", + "note": "IN FLIGHT: vangel76 PR #325; reserved until the command lands" + }, + { + "seq": "AFE", + "kind": "command", + "target": "`data.analyzeData`", + "pending": true, + "excel": "Analyze Data", + "note": "AI feature GAP; reserved until the command lands" + }, + { + "seq": "AFF", + "kind": "command", + "target": "edit.flashFill", + "excel": "Flash Fill", + "note": "" + }, + { + "seq": "AFO", + "kind": "command", + "target": "data.fromOtherSources", + "pending": true, + "excel": "Get External Data: From Other Sources", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "AFT", + "kind": "command", + "target": "data.fromText", + "pending": true, + "excel": "Get External Data: From Text", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "AFW", + "kind": "command", + "target": "data.fromWeb", + "pending": true, + "excel": "Get External Data: From Web", + "note": "reserved until the command lands" + }, + { + "seq": "AG", + "kind": "continue", + "excel": "Group Menu", + "note": "group level: containment(Group)" + }, + { + "seq": "AGA", + "kind": "command", + "target": "data.autoOutline", + "pending": true, + "excel": "Auto Outline", + "note": "Auto Outline; reserved until the command lands" + }, + { + "seq": "AGG", + "kind": "command", + "target": "data.group", + "excel": "Group", + "note": "" + }, + { + "seq": "AH", + "kind": "command", + "target": "data.hideDetail", + "excel": "Hide Detail", + "note": "" + }, + { + "seq": "AJ", + "kind": "command", + "target": "data.showDetail", + "excel": "Show Detail", + "note": "" + }, + { + "seq": "AK", + "kind": "command", + "target": "data.queriesConnections", + "excel": "Edit Links", + "note": "Edit Links (Queries & Connections);" + }, + { + "seq": "AM", + "kind": "command", + "target": "data.removeDuplicates", + "excel": "Remove Duplicates", + "note": "" + }, + { + "seq": "AO", + "kind": "command", + "target": "data.queriesConnections", + "excel": "Queries & Connections", + "note": "" + }, + { + "seq": "AP1", + "kind": "command", + "target": "file.properties", + "excel": "Properties", + "note": "" + }, + { + "seq": "APN", + "kind": "continue", + "excel": "Get Data (Power Query) Menu", + "note": "group level: Power Query variants = GAP" + }, + { + "seq": "APND", + "kind": "command", + "target": "data.fromDatabase", + "pending": true, + "excel": "From Database (Microsoft Query)", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNE", + "kind": "command", + "target": "data.powerQueryEditor", + "pending": true, + "excel": "Launch Power Query Editor", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNG", + "kind": "command", + "target": "data.getData", + "excel": "Get Data (Power Query)", + "note": "containment(Get Data)" + }, + { + "seq": "APNH", + "kind": "command", + "target": "data.fromHtml", + "pending": true, + "excel": "From HTML", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNL", + "kind": "command", + "target": "data.fromSqlServer", + "pending": true, + "excel": "From SQL Server ODBC", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNS", + "kind": "command", + "target": "data.dataSourceSettings", + "pending": true, + "excel": "Data Source Settings", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNT", + "kind": "command", + "target": "data.fromTextLegacy", + "pending": true, + "excel": "From Text (Legacy)", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APR", + "kind": "command", + "target": "data.recentSources", + "pending": true, + "excel": "Recent Sources", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APS", + "kind": "command", + "target": "data.queriesConnections", + "excel": "Show Queries", + "note": "" + }, + { + "seq": "APT", + "kind": "command", + "target": "data.fromTable", + "pending": true, + "excel": "From Table (query)", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "AQ", + "kind": "command", + "target": "data.advancedFilter", + "excel": "Advanced Filter", + "note": "" + }, + { + "seq": "AR", + "kind": "continue", + "note": "group: group: Queries & Connections" + }, + { + "seq": "ARA", + "kind": "command", + "target": "data.refreshAll", + "excel": "Refresh All", + "note": "" + }, + { + "seq": "ARC", + "kind": "command", + "target": "data.cancelRefresh", + "pending": true, + "excel": "Cancel Refresh", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "ARO", + "kind": "command", + "target": "data.connectionProperties", + "pending": true, + "excel": "Connection Properties", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "ARR", + "kind": "command", + "target": "pivot.refresh", + "excel": "Refresh", + "note": "" + }, + { + "seq": "ARS", + "kind": "command", + "target": "data.refreshStatus", + "pending": true, + "excel": "Refresh Status", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "ASA", + "kind": "command", + "target": "data.sortAscending", + "excel": "Sort A to Z", + "note": "" + }, + { + "seq": "ASD", + "kind": "command", + "target": "data.sortDescending", + "excel": "Sort Z to A", + "note": "" + }, + { + "seq": "ASS", + "kind": "command", + "target": "data.sort", + "excel": "Sort", + "note": "" + }, + { + "seq": "AT", + "kind": "command", + "target": "data.filter", + "excel": "Filter", + "note": "" + }, + { + "seq": "AU", + "kind": "continue", + "excel": "Ungroup Menu", + "note": "group level: containment(Ungroup)" + }, + { + "seq": "AUC", + "kind": "command", + "target": "data.clearOutline", + "pending": true, + "excel": "Clear Outline", + "note": "feature gap; reserved until the command lands" + }, + { + "seq": "AUU", + "kind": "command", + "target": "data.ungroup", + "excel": "Ungroup", + "note": "" + }, + { + "seq": "AV", + "kind": "continue", + "excel": "Data Validation Menu", + "note": "group level: containment(Data Validation)" + }, + { + "seq": "AVI", + "kind": "command", + "target": "data.circleInvalid", + "excel": "Circle Invalid Data", + "note": "" + }, + { + "seq": "AVR", + "kind": "command", + "target": "data.clearValidationCircles", + "pending": true, + "excel": "Clear Validation Circles", + "note": "reserved until the command lands" + }, + { + "seq": "AVV", + "kind": "command", + "target": "data.validation", + "excel": "Data Validation", + "note": "" + }, + { + "seq": "AW", + "kind": "continue", + "note": "group: group: What-If (goalSeek/scenarioManager/dataTable)" + }, + { + "seq": "AWG", + "kind": "command", + "target": "data.goalSeek", + "excel": "Goal Seek", + "note": "" + }, + { + "seq": "AWS", + "kind": "command", + "target": "data.scenarioManager", + "excel": "Scenario Manager", + "note": "" + }, + { + "seq": "AWT", + "kind": "command", + "target": "data.dataTable", + "excel": "Data Table", + "note": "" + }, + { + "seq": "AX", + "kind": "command", + "target": "data.existingConnections", + "pending": true, + "excel": "Existing Connections", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "AY", + "kind": "command", + "target": "data.reapply", + "excel": "Reapply Filter", + "note": "containment(Reapply)" + }, + { + "seq": "RA1", + "kind": "command", + "target": "review.checkAccessibility", + "excel": "Check Accessibility", + "note": "" + }, + { + "seq": "RA2", + "kind": "command", + "target": "review.showComments", + "pending": true, + "excel": "Show All Comments", + "note": "Show All Comments; reserved until the command lands" + }, + { + "seq": "RC", + "kind": "command", + "target": "review.newComment", + "excel": "New Comment", + "note": "" + }, + { + "seq": "RD", + "kind": "command", + "target": "review.deleteComment", + "excel": "Delete Comment", + "note": "" + }, + { + "seq": "RG", + "kind": "command", + "target": "review.showChanges", + "pending": true, + "excel": "Track Changes", + "note": "reserved until the command lands" + }, + { + "seq": "RL", + "kind": "command", + "target": "`review.translate`", + "pending": true, + "excel": "Translate", + "note": "feature gap; reserved until the command lands" + }, + { + "seq": "RN", + "kind": "command", + "target": "review.nextComment", + "pending": true, + "excel": "Next Comment", + "note": "Next Comment; reserved until the command lands" + }, + { + "seq": "RO", + "kind": "command", + "target": "review.protectWorkbook", + "excel": "Protect and Share Workbook" + }, + { + "seq": "RPS", + "kind": "command", + "target": "review.protectSheet", + "excel": "Protect Sheet", + "note": "" + }, + { + "seq": "RPW", + "kind": "command", + "target": "review.protectWorkbook", + "excel": "Protect Workbook", + "note": "" + }, + { + "seq": "RRS", + "kind": "command", + "target": "`review.smartLookup`", + "pending": true, + "excel": "Smart Lookup", + "note": "feature gap; reserved until the command lands" + }, + { + "seq": "RS", + "kind": "command", + "target": "review.spelling", + "excel": "Spelling", + "note": "" + }, + { + "seq": "RTN", + "kind": "command", + "target": "review.newNote", + "excel": "New Note", + "note": "" + }, + { + "seq": "RV", + "kind": "command", + "target": "review.previousComment", + "pending": true, + "excel": "Previous Comment", + "note": "Previous Comment; reserved until the command lands" + }, + { + "seq": "RW", + "kind": "command", + "target": "review.shareWorkbook", + "pending": true, + "excel": "Share Workbook", + "note": "reserved until the command lands" + }, + { + "seq": "WA", + "kind": "command", + "target": "view.arrangeAll", + "excel": "Arrange All", + "note": "" + }, + { + "seq": "WB", + "kind": "command", + "target": "view.sideBySide", + "pending": true, + "excel": "View Side by Side", + "note": "reserved until the command lands" + }, + { + "seq": "WC", + "kind": "command", + "target": "view.customViews", + "excel": "Custom Views", + "note": "" + }, + { + "seq": "WF", + "kind": "continue", + "excel": "Freeze Panes Menu", + "note": "group level:" + }, + { + "seq": "WFC", + "kind": "command", + "target": "view.freezeFirstColumn", + "excel": "Freeze First Column", + "note": "" + }, + { + "seq": "WFF", + "kind": "command", + "target": "view.freezePanes", + "excel": "Freeze Panes", + "note": "" + }, + { + "seq": "WFR", + "kind": "command", + "target": "view.freezeTopRow", + "excel": "Freeze Top Row", + "note": "" + }, + { + "seq": "WG", + "kind": "command", + "target": "view.zoomToSelection", + "excel": "Zoom To Selection", + "note": "" + }, + { + "seq": "WH", + "kind": "command", + "target": "view.hideWindow", + "excel": "Hide Window", + "note": "" + }, + { + "seq": "WI", + "kind": "command", + "target": "view.pageBreakPreview", + "excel": "Page Break Preview", + "note": "" + }, + { + "seq": "WJ", + "kind": "command", + "target": "view.zoom", + "excel": "Zoom To 100%", + "note": "" + }, + { + "seq": "WL", + "kind": "command", + "target": "view.normal", + "excel": "Normal View", + "note": "containment(Normal)" + }, + { + "seq": "WM", + "kind": "continue", + "excel": "Macros Menu", + "note": "group level: containment(Macros)" + }, + { + "seq": "WMR", + "kind": "command", + "target": "view.macros", + "excel": "Record Macro", + "note": "Record Macro (macros menu)" + }, + { + "seq": "WMV", + "kind": "command", + "target": "view.macros", + "excel": "View Macros", + "note": "containment(Macros)" + }, + { + "seq": "WN", + "kind": "command", + "target": "view.newWindow", + "excel": "New Window", + "note": "" + }, + { + "seq": "WP", + "kind": "command", + "target": "view.pageLayout", + "excel": "Page Layout View", + "note": "" + }, + { + "seq": "WQ", + "kind": "command", + "target": "view.zoom", + "excel": "Zoom", + "note": "" + }, + { + "seq": "WR", + "kind": "command", + "target": "view.ruler", + "excel": "Ruler", + "note": "" + }, + { + "seq": "WS", + "kind": "command", + "target": "view.split", + "excel": "Split", + "note": "" + }, + { + "seq": "WT", + "kind": "command", + "target": "view.resetWindowPosition", + "pending": true, + "excel": "Reset Window Position", + "note": "reserved until the command lands" + }, + { + "seq": "WU", + "kind": "command", + "target": "view.unhideWindow", + "excel": "Unhide Window", + "note": "" + }, + { + "seq": "WVC", + "kind": "command", + "target": "view.focusCell", + "pending": true, + "excel": "Focus Cell Menu", + "note": "Focus Cell; reserved until the command lands" + }, + { + "seq": "WVF", + "kind": "command", + "target": "view.formulaBar", + "pending": true, + "excel": "Toggle Formula Bar", + "note": "Toggle Formula Bar; reserved until the command lands" + }, + { + "seq": "WVH", + "kind": "command", + "target": "view.headings", + "excel": "Toggle Headings", + "note": "containment(Headings)" + }, + { + "seq": "WVL", + "kind": "command", + "target": "view.synchronousScrolling", + "pending": true, + "excel": "Synchronous Scrolling", + "note": "Synchronous Scrolling; reserved until the command lands" + }, + { + "seq": "WVN", + "kind": "command", + "target": "view.navigation", + "pending": true, + "excel": "Navigation", + "note": "Navigation pane; reserved until the command lands" + }, + { + "seq": "WVR", + "kind": "command", + "target": "view.ruler", + "excel": "Ruler", + "note": "" + }, + { + "seq": "WVV", + "kind": "command", + "target": "view.sideBySide", + "pending": true, + "excel": "View Side by Side", + "note": "reserved until the command lands" + }, + { + "seq": "WW", + "kind": "command", + "target": "view.switchWindows", + "excel": "Switch Windows", + "note": "" + }, + { + "seq": "JAM", + "kind": "command", + "target": "chart.formatSelection", + "excel": "Chart format: Format Selection pane", + "note": "containment(Format Selection)" + }, + { + "seq": "JCA", + "kind": "command", + "target": "chart.addElement", + "excel": "Chart: Add Chart Element", + "note": "containment(Add Chart Element)" + }, + { + "seq": "JCC", + "kind": "command", + "target": "chart.changeType", + "excel": "Chart: Change Chart Type", + "note": "containment(Change Chart Type)" + }, + { + "seq": "JTS", + "kind": "command", + "target": "table.style", + "excel": "Table: table style", + "note": "" + }, + { + "seq": "JTV", + "kind": "command", + "target": "table.summarizePivot", + "excel": "Table: Summarize with PivotTable", + "note": "containment(Summarize with PivotTable)" + }, + { + "seq": "FA", + "kind": "command", + "excel": "Save / Save As page", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "file.saveAs" + }, + { + "seq": "FC", + "kind": "command", + "target": "file.close", + "excel": "Close workbook" + }, + { + "seq": "FD", + "kind": "command", + "target": "file.account", + "pending": true, + "excel": "Account page", + "note": "Account page; reserved until the command lands" + }, + { + "seq": "FEA", + "kind": "command", + "target": "file.exportPdf", + "excel": "Export: create PDF/XPS", + "note": "Export: create PDF/XPS" + }, + { + "seq": "FEC", + "kind": "command", + "target": "file.saveAs", + "excel": "Export: change file type", + "note": "Export: change file type" + }, + { + "seq": "FH", + "kind": "command", + "target": "file.home", + "pending": true, + "excel": "File Home page (recent/pinned/shared/new blank)", + "note": "File Home page; reserved until the command lands" + }, + { + "seq": "FI", + "kind": "command", + "excel": "Info page", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "file.properties" + }, + { + "seq": "FN", + "kind": "command", + "target": "file.new", + "excel": "New page", + "note": "New page" + }, + { + "seq": "FO", + "kind": "command", + "excel": "Open page", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "file.open" + }, + { + "seq": "FP", + "kind": "command", + "target": "file.print", + "excel": "Print page", + "note": "containment(Print)" + }, + { + "seq": "FT", + "kind": "command", + "target": "file.options", + "pending": true, + "excel": "Options dialog", + "note": "Options dialog; reserved until the command lands" + }, + { + "seq": "Q", + "kind": "command", + "target": "ui.search", + "pending": true, + "excel": "Search / Tell Me", + "note": "Alt+Q search box does not exist yet (issue #49)" + }, + { + "seq": "6", + "kind": "command", + "target": "qat.6", + "pending": true, + "excel": "Quick Access Toolbar position 6", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "7", + "kind": "command", + "target": "qat.7", + "pending": true, + "excel": "Quick Access Toolbar position 7", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "8", + "kind": "command", + "target": "qat.8", + "pending": true, + "excel": "Quick Access Toolbar position 8", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "9", + "kind": "command", + "target": "qat.9", + "pending": true, + "excel": "Quick Access Toolbar position 9", + "note": "QAT navigation does not exist yet" + } ] -} +} \ No newline at end of file diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index 7da90f5..81a1ff6 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -174,6 +174,7 @@ fn keyboard_opened_paste_menu_accepts_a_mouse_click_after_keytips_cancel() { #[test] fn legacy_paste_special_keeps_formulas_when_requested() { let mut h = harness(copied_row()); + // The legacy Edit route opens the dialog directly; F is a dialog mnemonic. sequence(&mut h, &[Key::E, Key::S], false); assert_eq!(h.state().dialog.as_ref().unwrap().name(), "pasteSpecial"); letter(&mut h, Key::F, false); @@ -306,6 +307,12 @@ fn tab_letters_switch_to_their_ribbon_tabs() { key(&mut h, Key::F10); // disarm assert_eq!(h.state().keytips.prefix(), None); } + // A terminal tab unit with no deeper sequences finishes and clears. + letter(&mut h, Key::J, false); + assert_eq!(h.state().keytips.prefix(), Some("J"), "J waits for its multi-letter unit"); + letter(&mut h, Key::I, false); + assert_eq!(h.state().ui.ribbon_tab, "Draw"); + assert_eq!(h.state().keytips.prefix(), None, "terminal tab units finish cleanly"); } #[test] diff --git a/xtask/src/keytips.rs b/xtask/src/keytips.rs index 286a043..baa498c 100644 --- a/xtask/src/keytips.rs +++ b/xtask/src/keytips.rs @@ -8,10 +8,34 @@ use std::path::Path; const TABS: &[&str] = &["Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"]; +/// Format generated source through `rustfmt` so the checked-in file matches +/// the emitter byte-for-byte. +fn rustfmt(src: &str) -> Result { + use std::io::Write; + use std::process::{Command, Stdio}; + let mut child = Command::new("rustfmt") + .args(["--edition", "2024", "--emit", "stdout"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("spawn rustfmt: {e}"))?; + let stdin = child.stdin.as_mut().ok_or("rustfmt: failed to open stdin")?; + stdin.write_all(src.as_bytes()).map_err(|e| format!("write rustfmt stdin: {e}"))?; + let out = child.wait_with_output().map_err(|e| format!("wait rustfmt: {e}"))?; + if !out.status.success() { + return Err(format!("rustfmt failed: {}", String::from_utf8_lossy(&out.stderr))); + } + Ok(String::from_utf8_lossy(&out.stdout).into_owned()) +} + pub fn run(root: &Path, rest: &[&str]) -> Result<(), String> { let json_path = root.join("crates/ui-egui/src/keytips/keytips.json"); let out_path = root.join("crates/ui-egui/src/keytips/keytip_table/generated.rs"); - let generated = generate(&fs::read_to_string(&json_path).map_err(|e| format!("read {}: {e}", json_path.display()))?)?; + let raw = generate(&fs::read_to_string(&json_path).map_err(|e| format!("read {}: {e}", json_path.display()))?)?; + // The table is checked into the tree formatted, so emit through rustfmt: + // otherwise `--check` always fails against the fmt'd file. + let generated = rustfmt(&raw)?; if rest.contains(&"--check") { let on_disk = fs::read_to_string(&out_path).map_err(|e| format!("read {}: {e}", out_path.display()))?; if on_disk != generated { @@ -34,8 +58,8 @@ pub fn generate(source: &str) -> Result { for (i, entry) in seqs.iter().enumerate() { let at = format!("entry {}", i + 1); let seq = entry.get("seq").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `seq`"))?; - if seq.is_empty() || seq.len() > 4 || !seq.bytes().all(|b| b.is_ascii_alphanumeric()) { - return Err(format!("{at}: seq {seq:?} must be 1-4 ASCII letters/digits")); + if seq.is_empty() || seq.len() > 6 || !seq.bytes().all(|b| b.is_ascii_alphanumeric()) { + return Err(format!("{at}: seq {seq:?} must be 1-6 ASCII letters/digits (Excel's longest are 5, e.g. sparkline paths)")); } if !seen.insert(seq.to_string()) { return Err(format!("{at}: duplicate seq {seq:?}")); From 95b36417250f2a948ba452f419a76eddcaf5dca2 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 20:11:34 +0300 Subject: [PATCH 06/15] Paint keytip badges on every ribbon control (issue #49) The badge painter only covered five sites, so pressing Alt+H showed nothing past the tab letter. Now every control with a catalog command id paints its badge automatically: - control_badge(prefix, id): the rest of a control's unique sequence (HFC -> "FC" on Font Color), or the common group letter when many variants land on one control (HBA..HBW -> "B" on Borders). - anchor_badge(prefix, targets): menu anchors resolve to the dominant group-level candidate (Pictures shows "P", not SmartArt's "M"). - ribbon.rs: 90+ control sites now paint through control_keytip/ anchor_keytip (mechanical sweep + hand-wired anchors); split buttons paint at their arrow rect. - Offscreen snapshot verified: at prefix H the Home ribbon paints V/X/C/FP/1/2/3U/FC/H/FG/FK/AL/AC/AR/5/6/WW/MC/K/0/9/L/T/J/I/D/O. - Tests: 29 lib + 14 keytips, all 8 CI gates green. --- crates/ui-egui/src/keytips/keytip_table.rs | 71 ++++ crates/ui-egui/src/ribbon.rs | 384 +++++++++++++++++---- 2 files changed, 387 insertions(+), 68 deletions(-) diff --git a/crates/ui-egui/src/keytips/keytip_table.rs b/crates/ui-egui/src/keytips/keytip_table.rs index 8caa4d6..da59a2f 100644 --- a/crates/ui-egui/src/keytips/keytip_table.rs +++ b/crates/ui-egui/src/keytips/keytip_table.rs @@ -101,6 +101,57 @@ pub fn control_badges(prefix: &str, target: &str) -> Vec<&'static str> { sequences_for(target).into_iter().filter(|seq| seq.len() > prefix.len() && seq.starts_with(prefix)).collect() } +/// The badge for a single control: the rest of its finish sequence when it has exactly one, or +/// the common group letter when several sequences land on the same control (menu anchors like +/// Borders, whose HBA..HBW variants all share "HB"). +pub fn control_badge(prefix: &str, target: &str) -> Option<&'static str> { + if prefix.is_empty() { + return None; // empty prefix shows only tab badges + } + let seqs: Vec<&'static str> = sequences_for(target).into_iter().filter(|seq| seq.len() > prefix.len() && seq.starts_with(prefix)).collect(); + match seqs.as_slice() { + [] => None, + [single] => Some(single), + many => { + let common = |a: &str, b: &str| a.chars().zip(b.chars()).take_while(|(x, y)| x == y).count(); + let lcp = many.iter().skip(1).fold(many[0].len(), |acc, s| acc.min(common(many[0], s))); + (lcp > prefix.len()).then_some(&many[0][..lcp]) + } + } +} + +/// The single badge for a menu anchor: the direct finish one key past the prefix, or the +/// group-level candidate that prefixes the most of `targets`' sequences. Anchors get one +/// badge, so overlapping groups (e.g. Pictures' "NP" vs SmartArt's "NM") resolve to the +/// dominant one. +pub fn anchor_badge(prefix: &str, targets: &[&str]) -> Option<&'static str> { + if prefix.is_empty() { + return None; // empty prefix shows only tab badges + } + let all: Vec<&'static str> = targets.iter().flat_map(|t| sequences_for(t)).filter(|s| s.len() > prefix.len() && s.starts_with(prefix)).collect(); + if all.is_empty() { + return None; + } + let depth = prefix.len() + 1; + let mut candidates: Vec<&'static str> = all.iter().filter(|s| s.len() == depth).copied().collect(); + if candidates.is_empty() { + for s in &all { + let c = &s[..depth.min(s.len())]; + if is_group_level(c) && !candidates.contains(&c) { + candidates.push(c); + } + } + } + let mut best: Option<(&'static str, usize)> = None; + for c in candidates { + let score = all.iter().filter(|s| s.starts_with(c)).count(); + if best.is_none_or(|(_, b)| score > b) { + best = Some((c, score)); + } + } + best.map(|(c, _)| c) +} + #[cfg(test)] mod tests { use super::*; @@ -160,4 +211,24 @@ mod tests { assert_eq!(control_badges("HA", "home.alignRight"), vec!["HAR"]); assert!(control_badges("HZ", "home.alignRight").is_empty(), "unknown prefixes show nothing"); } + + #[test] + fn control_badge_shows_the_rest_or_the_group_letter() { + assert_eq!(control_badge("H", "home.bold"), Some("H1"), "a unique sequence paints its full rest"); + assert_eq!(control_badge("H", "home.fontColor"), Some("HFC")); + assert_eq!(control_badge("H", "home.borders"), Some("HB"), "many variants collapse to the group letter"); + assert!(control_badge("", "home.bold").is_none(), "empty prefix shows only tab badges"); + assert!(control_badge("HZ", "home.bold").is_none(), "unknown prefixes show nothing"); + assert!(control_badge("H", "view.zoom").is_none(), "diverging sequences at the next letter show nothing"); + } + + #[test] + fn anchor_badge_resolves_to_the_dominant_group() { + assert_eq!(anchor_badge("H", &["edit.paste", "edit.pasteSpecial", "dialog:pasteSpecial"]), Some("HV")); + assert_eq!(anchor_badge("P", &["pageLayout.orientation"]), Some("PO"), "a direct finish paints itself"); + assert_eq!(anchor_badge("N", &["insert.picture"]), Some("NP"), "NP dominates the lone NM (SmartArt) entry"); + assert_eq!(anchor_badge("H", &["table.style"]), Some("HT")); + assert!(anchor_badge("", &["edit.paste"]).is_none(), "empty prefix shows only tab badges"); + assert!(anchor_badge("H", &["view.zoom"]).is_none(), "no sequences under this prefix"); + } } diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index dff93f0..2b80957 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -63,7 +63,9 @@ pub fn title_bar(app: &mut SheetApp, ui: &mut Ui) { app.grid.toggle_autosave(); } ui.add_space(6.0); - if icon_button(ui, Icon::Home, t.text_dim, "Home", vec2(26.0, ch)).clicked() { + let __k = icon_button(ui, Icon::Home, t.text_dim, "Home", vec2(26.0, ch)); + control_keytip(app, ui, &__k, "start"); + if __k.clicked() { app.open_dialog("start", json!({})); } if icon_button(ui, Icon::Save, t.text_dim, "Save (⌘S)", vec2(26.0, ch)).clicked() @@ -122,7 +124,9 @@ pub fn title_bar(app: &mut SheetApp, ui: &mut Ui) { ); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.add_space(8.0); - if icon_button(ui, Icon::Search, t.text_dim, "Search commands (⌘⇧Space)", vec2(28.0, ch)).clicked() { + let __k = icon_button(ui, Icon::Search, t.text_dim, "Search commands (⌘⇧Space)", vec2(28.0, ch)); + control_keytip(app, ui, &__k, "commandSearch"); + if __k.clicked() { app.open_dialog("commandSearch", json!({})); } let share = small_button(ui, Icon::Share, "Share", "Share: export or save a copy", true); @@ -135,7 +139,9 @@ pub fn title_bar(app: &mut SheetApp, ui: &mut Ui) { } } }); - if icon_button(ui, Icon::Comment, t.text_dim, "Comments", vec2(28.0, ch)).clicked() { + let __k = icon_button(ui, Icon::Comment, t.text_dim, "Comments", vec2(28.0, ch)); + control_keytip(app, ui, &__k, "comments"); + if __k.clicked() { app.open_dialog("comments", json!({})); } // Quick light/dark switch (#194); right-click offers System as well. The same @@ -323,6 +329,20 @@ fn keytip(app: &SheetApp, ui: &Ui, sequence: &str, at: egui::Pos2) { painter.galley(rect.min + vec2(3.5, 1.5), text, t.text); } +/// Paint the badge for a control that runs command `id`, when the armed prefix matches. +fn control_keytip(app: &SheetApp, ui: &Ui, response: &egui::Response, id: &str) { + if let Some(sequence) = keytip_table::control_badge(app.keytips.prefix().unwrap_or_default(), id) { + keytip(app, ui, sequence, response.rect.center_bottom()); + } +} + +/// Paint the single badge for a menu anchor whose popup items run `targets`. +fn anchor_keytip(app: &SheetApp, ui: &Ui, response: &egui::Response, targets: &[&str]) { + if let Some(sequence) = keytip_table::anchor_badge(app.keytips.prefix().unwrap_or_default(), targets) { + keytip(app, ui, sequence, response.rect.center_bottom()); + } +} + fn menu_items(app: &mut SheetApp, ui: &mut Ui, items: &[(&str, &str, serde_json::Value)]) { for (label, id, p) in items { if *label == "-" { @@ -403,14 +423,19 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ); }); ui.vertical(|ui| { - if icon_button(ui, Icon::Cut, t.text, "Cut (⌘X)", vec2(26.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::Cut, t.text, "Cut (⌘X)", vec2(26.0, 23.0)); + control_keytip(app, ui, &__k, "edit.cut"); + if __k.clicked() { app.copy_to_clipboard(ui.ctx(), "edit.cut"); } - if icon_button(ui, Icon::Copy, t.text, "Copy (⌘C)", vec2(26.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::Copy, t.text, "Copy (⌘C)", vec2(26.0, 23.0)); + control_keytip(app, ui, &__k, "edit.copy"); + if __k.clicked() { app.copy_to_clipboard(ui.ctx(), "edit.copy"); } let fp_on = app.session.format_painter.is_some(); let fp = toggle_button(ui, Icon::Brush, fp_on, "Format Painter (double-click to keep it on)"); + control_keytip(app, ui, &fp, "edit.formatPainter"); if fp.double_clicked() { act(app, "edit.formatPainter", json!({"sticky": true})); } else if fp.clicked() { @@ -427,21 +452,30 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ui.horizontal(|ui| { font_combo(app, ui, &st); size_combo(app, ui, &st); - if icon_button(ui, Icon::Plus, t.text, "Increase Font Size", vec2(22.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::Plus, t.text, "Increase Font Size", vec2(22.0, 23.0)); + control_keytip(app, ui, &__k, "home.increaseFontSize"); + if __k.clicked() { act(app, "home.increaseFontSize", json!({})); } - if icon_button(ui, Icon::Minus, t.text, "Decrease Font Size", vec2(22.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::Minus, t.text, "Decrease Font Size", vec2(22.0, 23.0)); + control_keytip(app, ui, &__k, "home.decreaseFontSize"); + if __k.clicked() { act(app, "home.decreaseFontSize", json!({})); } }); ui.horizontal(|ui| { - if toggle_button(ui, Icon::Bold, st.font.bold, "Bold (⌘B)").clicked() { + let __k = toggle_button(ui, Icon::Bold, st.font.bold, "Bold (⌘B)"); + control_keytip(app, ui, &__k, "home.bold"); + if __k.clicked() { act(app, "home.bold", json!({})); } - if toggle_button(ui, Icon::Italic, st.font.italic, "Italic (⌘I)").clicked() { + let __k = toggle_button(ui, Icon::Italic, st.font.italic, "Italic (⌘I)"); + control_keytip(app, ui, &__k, "home.italic"); + if __k.clicked() { act(app, "home.italic", json!({})); } let (u, ua) = split_button(ui, Icon::Underline, None, "Underline (⌘U)"); + control_keytip(app, ui, &ua, "home.underline"); if u { act(app, "home.underline", json!({})); } @@ -455,10 +489,13 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ], ); }); - if toggle_button(ui, Icon::Strike, st.font.strike, "Strikethrough").clicked() { + let __k = toggle_button(ui, Icon::Strike, st.font.strike, "Strikethrough"); + control_keytip(app, ui, &__k, "home.strikethrough"); + if __k.clicked() { act(app, "home.strikethrough", json!({})); } let (b, ba) = split_button(ui, Icon::Borders, None, "Borders"); + control_keytip(app, ui, &ba, "home.borders"); if b { act(app, "home.borders", json!({"preset": app.grid.last_border.clone()})); } @@ -493,6 +530,7 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { let fill = app.grid.last_fill.clone(); let fill_c = gridcraft_engine::model::Color::from_hex(&fill).and_then(|c| c.resolve(&Default::default())).map(theme::color32); let (f, fa) = split_button(ui, Icon::Fill, fill_c, "Fill Color"); + control_keytip(app, ui, &fa, "home.fillColor"); if f { act(app, "home.fillColor", json!({"color": fill})); } @@ -508,6 +546,7 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { let fc = app.grid.last_font_color.clone(); let fc_c = gridcraft_engine::model::Color::from_hex(&fc).and_then(|c| c.resolve(&Default::default())).map(theme::color32); let (c, ca) = split_button(ui, Icon::FontColor, fc_c, "Font Color"); + control_keytip(app, ui, &ca, "home.fontColor"); if c { act(app, "home.fontColor", json!({"color": fc})); } @@ -545,6 +584,9 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { } } let o = small_button(ui, Icon::Orientation, "", "Orientation", true); + for sequence in keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["home.orientation", "dialog:formatCells"]) { + keytip(app, ui, sequence, o.rect.center_bottom()); + } egui::Popup::menu(&o).show(|ui| { menu_items( app, @@ -562,6 +604,7 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ); }); let wrap = toggle_button(ui, Icon::Wrap, st.align.wrap, "Wrap Text"); + control_keytip(app, ui, &wrap, "home.wrapText"); if wrap.clicked() { act(app, "home.wrapText", json!({})); } @@ -581,13 +624,18 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { act(app, id, json!({})); } } - if icon_button(ui, Icon::IndentDec, t.text, "Decrease Indent", vec2(24.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::IndentDec, t.text, "Decrease Indent", vec2(24.0, 23.0)); + control_keytip(app, ui, &__k, "home.decreaseIndent"); + if __k.clicked() { act(app, "home.decreaseIndent", json!({})); } - if icon_button(ui, Icon::IndentInc, t.text, "Increase Indent", vec2(24.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::IndentInc, t.text, "Increase Indent", vec2(24.0, 23.0)); + control_keytip(app, ui, &__k, "home.increaseIndent"); + if __k.clicked() { act(app, "home.increaseIndent", json!({})); } let (m, ma) = split_button(ui, Icon::Merge, None, "Merge & Center"); + control_keytip(app, ui, &ma, "home.mergeCenter"); if m { act(app, "home.mergeCenter", json!({})); } @@ -612,6 +660,7 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { number_combo(app, ui, &st); ui.horizontal(|ui| { let (c, ca) = split_button(ui, Icon::Currency, None, "Accounting Number Format"); + control_keytip(app, ui, &ca, "home.accounting"); if c { act(app, "home.accounting", json!({})); } @@ -641,16 +690,24 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ], ); }); - if icon_button(ui, Icon::Percent, t.text, "Percent Style", vec2(24.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::Percent, t.text, "Percent Style", vec2(24.0, 23.0)); + control_keytip(app, ui, &__k, "home.percent"); + if __k.clicked() { act(app, "home.percent", json!({})); } - if icon_button(ui, Icon::Comma, t.text, "Comma Style", vec2(24.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::Comma, t.text, "Comma Style", vec2(24.0, 23.0)); + control_keytip(app, ui, &__k, "home.comma"); + if __k.clicked() { act(app, "home.comma", json!({})); } - if icon_button(ui, Icon::DecInc, t.text, "Increase Decimal", vec2(26.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::DecInc, t.text, "Increase Decimal", vec2(26.0, 23.0)); + control_keytip(app, ui, &__k, "home.increaseDecimal"); + if __k.clicked() { act(app, "home.increaseDecimal", json!({})); } - if icon_button(ui, Icon::DecDec, t.text, "Decrease Decimal", vec2(26.0, 23.0)).clicked() { + let __k = icon_button(ui, Icon::DecDec, t.text, "Decrease Decimal", vec2(26.0, 23.0)); + control_keytip(app, ui, &__k, "home.decreaseDecimal"); + if __k.clicked() { act(app, "home.decreaseDecimal", json!({})); } }); @@ -658,15 +715,24 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { sep(ui); // Styles let cf = big_button(ui, Icon::CondFormat, "Conditional\nFormatting", "Conditional Formatting", true); + anchor_keytip(app, ui, &cf, &["home.conditionalFormat", "home.manageRules", "home.clearRules"]); egui::Popup::menu(&cf).show(|ui| cf_menu(app, ui)); let ft = big_button(ui, Icon::FormatTable, "Format\nas Table", "Format as Table", true); + anchor_keytip(app, ui, &ft, &["table.style"]); egui::Popup::menu(&ft).show(|ui| table_gallery(app, ui, "home.formatAsTable")); let cs = big_button(ui, Icon::CellStyles, "Cell\nStyles", "Cell Styles", true); + anchor_keytip(app, ui, &cs, &["home.newCellStyle"]); egui::Popup::menu(&cs).show(|ui| cell_style_gallery(app, ui)); sep(ui); // Cells ui.vertical(|ui| { let ins = small_button(ui, Icon::Insert, "Insert", "Insert", true); + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &["dialog:insertCells", "home.insertRows", "home.insertColumns", "home.insertSheet"], + ) { + keytip(app, ui, sequence, ins.rect.center_bottom()); + } egui::Popup::menu(&ins).show(|ui| { menu_items( app, @@ -680,6 +746,12 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ); }); let del = small_button(ui, Icon::Delete, "Delete", "Delete", true); + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &["dialog:deleteCells", "home.deleteRows", "home.deleteColumns", "home.deleteSheet"], + ) { + keytip(app, ui, sequence, del.rect.center_bottom()); + } egui::Popup::menu(&del).show(|ui| { menu_items( app, @@ -754,6 +826,7 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ui.vertical(|ui| { ui.horizontal(|ui| { let (s, sa) = split_button(ui, Icon::Sum, None, "AutoSum (⌘⇧T)"); + control_keytip(app, ui, &sa, "formulas.autoSum"); if s { act(app, "formulas.autoSum", json!({})); } @@ -770,6 +843,12 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { }); }); let fill = small_button(ui, Icon::FillDown, "", "Fill", true); + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &["edit.fillDown", "edit.fillRight", "edit.fillUp", "edit.fillLeft", "dialog:series", "edit.flashFill"], + ) { + keytip(app, ui, sequence, fill.rect.center_bottom()); + } egui::Popup::menu(&fill).show(|ui| { menu_items( app, @@ -785,6 +864,12 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ); }); let clear = small_button(ui, Icon::Eraser, "", "Clear", true); + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &["edit.clearAll", "edit.clearFormats", "edit.clearContents", "edit.clearComments", "edit.clearHyperlinks"], + ) { + keytip(app, ui, sequence, clear.rect.center_bottom()); + } egui::Popup::menu(&clear).show(|ui| { menu_items( app, @@ -800,6 +885,12 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { }); }); let sf = big_button(ui, Icon::SortFilter, "Sort &\nFilter", "Sort & Filter", true); + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &["data.sortAscending", "data.sortDescending", "dialog:sort", "data.filter", "data.clearFilter", "data.reapply"], + ) { + keytip(app, ui, sequence, sf.rect.center_bottom()); + } egui::Popup::menu(&sf).show(|ui| { menu_items( app, @@ -816,6 +907,12 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { ); }); let fs = big_button(ui, Icon::Find, "Find &\nSelect", "Find & Select", true); + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &["dialog:find", "dialog:goTo", "dialog:goToSpecial", "edit.goToSpecial"], + ) { + keytip(app, ui, sequence, fs.rect.center_bottom()); + } egui::Popup::menu(&fs).show(|ui| { menu_items( app, @@ -1146,14 +1243,19 @@ fn cell_style_gallery(app: &mut SheetApp, ui: &mut Ui) { } fn insert(app: &mut SheetApp, ui: &mut Ui) { - if big_button(ui, Icon::PivotTable, "PivotTable", "PivotTable", false).clicked() { + let __k = big_button(ui, Icon::PivotTable, "PivotTable", "PivotTable", false); + control_keytip(app, ui, &__k, "insert.pivotTable"); + if __k.clicked() { act(app, "insert.pivotTable", json!({})); } - if big_button(ui, Icon::Table, "Table", "Table (⌘T)", false).clicked() { + let __k = big_button(ui, Icon::Table, "Table", "Table (⌘T)", false); + control_keytip(app, ui, &__k, "insert.table"); + if __k.clicked() { act(app, "insert.table", json!({})); } sep(ui); let pic = big_button(ui, Icon::Picture, "Pictures", "Insert a picture", true); + anchor_keytip(app, ui, &pic, &["insert.picture"]); pic.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), "Pictures")); egui::Popup::menu(&pic).show(|ui| { for (label, placement) in [("Place in Cell", "cell"), ("Place over Cells", "overCells")] { @@ -1164,6 +1266,7 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { } }); let shapes = big_button(ui, Icon::Shapes, "Shapes", "Shapes", true); + anchor_keytip(app, ui, &shapes, &["insert.shape"]); egui::Popup::menu(&shapes).show(|ui| { for (label, kind) in [ ("Rectangle", "rectangle"), @@ -1180,6 +1283,7 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { } }); let icn = big_button(ui, Icon::Theme, "Icons", "Insert an icon", true); + anchor_keytip(app, ui, &icn, &["insert.icons"]); egui::Popup::menu(&icn).show(|ui| { ui.set_width(260.0); egui::Grid::new("icon_lib").show(ui, |ui| { @@ -1200,8 +1304,10 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { }); }); sep(ui); - if big_button(ui, Icon::Chart, "Recommended\nCharts", "Recommended Charts", false).clicked() { - app.open_dialog("recommendedCharts", json!({})); + let __k = big_button(ui, Icon::Chart, "Recommended\nCharts", "Recommended Charts", false); + control_keytip(app, ui, &__k, "insert.recommendedCharts"); + if __k.clicked() { + act(app, "insert.recommendedCharts", json!({})); } ui.vertical(|ui| { ui.horizontal(|ui| { @@ -1282,6 +1388,7 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { } sep(ui); let sp = big_button(ui, Icon::Sparkline, "Sparklines", "Sparklines", true); + anchor_keytip(app, ui, &sp, &["insert.sparkline"]); egui::Popup::menu(&sp).show(|ui| { for (label, kind) in [("Line", "line"), ("Column", "column"), ("Win/Loss", "winLoss")] { if ui.button(label).clicked() { @@ -1295,21 +1402,30 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { act(app, "insert.checkbox", json!({})); } sep(ui); - if big_button(ui, Icon::Link, "Link", "Insert Link (⌘K)", false).clicked() { + let __k = big_button(ui, Icon::Link, "Link", "Insert Link (⌘K)", false); + control_keytip(app, ui, &__k, "insertLink"); + if __k.clicked() { app.open_dialog("insertLink", json!({})); } - if big_button(ui, Icon::Comment, "Comment", "New Comment", false).clicked() { + let __k = big_button(ui, Icon::Comment, "Comment", "New Comment", false); + control_keytip(app, ui, &__k, "comment"); + if __k.clicked() { app.open_dialog("comment", json!({"threaded": true})); } sep(ui); - if big_button(ui, Icon::TextBox, "Text\nBox", "Text Box", false).clicked() { + let __k = big_button(ui, Icon::TextBox, "Text\nBox", "Text Box", false); + control_keytip(app, ui, &__k, "insert.textBox"); + if __k.clicked() { act(app, "insert.textBox", json!({})); } - if big_button(ui, Icon::PageLayout, "Header &\nFooter", "Header & Footer", false).clicked() { + let __k = big_button(ui, Icon::PageLayout, "Header &\nFooter", "Header & Footer", false); + control_keytip(app, ui, &__k, "headerFooter"); + if __k.clicked() { // As in Excel: Page Layout view, editing the header in place (the dialog is under Page Setup). crate::paper_view::start_editing(app); } let sym = big_button(ui, Icon::Symbol, "Symbol", "Symbol", true); + control_keytip(app, ui, &sym, "insert.symbol"); egui::Popup::menu(&sym).show(|ui| { ui.set_width(260.0); ui.horizontal_wrapped(|ui| { @@ -1327,7 +1443,9 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { fn draw(app: &mut SheetApp, ui: &mut Ui) { let tool = app.session.draw_tool.clone(); - if big_button(ui, Icon::Shapes, "Select\nObjects", "Select and move ink and shapes", false).clicked() { + let __k = big_button(ui, Icon::Shapes, "Select\nObjects", "Select and move ink and shapes", false); + control_keytip(app, ui, &__k, "draw.lasso"); + if __k.clicked() { act(app, "draw.lasso", json!({})); } sep(ui); @@ -1350,13 +1468,16 @@ fn draw(app: &mut SheetApp, ui: &mut Ui) { act(app, "draw.eraser", json!({})); } sep(ui); - if big_button(ui, Icon::Shapes, "Ink to\nShape", "Convert the last ink stroke to a shape", false).clicked() { + let __k = big_button(ui, Icon::Shapes, "Ink to\nShape", "Convert the last ink stroke to a shape", false); + control_keytip(app, ui, &__k, "draw.inkToShape"); + if __k.clicked() { act(app, "draw.inkToShape", json!({})); } } fn page_layout(app: &mut SheetApp, ui: &mut Ui) { let th = big_button(ui, Icon::Theme, "Themes", "Themes", true); + anchor_keytip(app, ui, &th, &["pageLayout.theme"]); egui::Popup::menu(&th).show(|ui| { for (name, colors) in gridcraft_engine::cmd::view::themes() { ui.horizontal(|ui| { @@ -1372,6 +1493,9 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { }); sep(ui); let m = big_button(ui, Icon::Margins, "Margins", "Margins", true); + for sequence in keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["pageLayout.margins", "dialog:pageSetup"]) { + keytip(app, ui, sequence, m.rect.center_bottom()); + } egui::Popup::menu(&m).show(|ui| { menu_items( app, @@ -1385,6 +1509,7 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { ); }); let o = big_button(ui, Icon::Orient, "Page Layout|Orientation", "Page Layout|Orientation", true); + control_keytip(app, ui, &o, "pageLayout.orientation"); egui::Popup::menu(&o).show(|ui| { menu_items( app, @@ -1396,6 +1521,7 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { ); }); let s = big_button(ui, Icon::Paper, "Size", "Paper Size", true); + control_keytip(app, ui, &s, "pageLayout.size"); egui::Popup::menu(&s).show(|ui| { for p in ["Letter", "Legal", "Tabloid", "Executive", "A3", "A4", "A5"] { if ui.button(p).clicked() { @@ -1404,6 +1530,7 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { } }); let pa = big_button(ui, Icon::PrintArea, "Print\nArea", "Print Area", true); + anchor_keytip(app, ui, &pa, &["pageLayout.printArea"]); egui::Popup::menu(&pa).show(|ui| { menu_items( app, @@ -1412,6 +1539,9 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { ); }); let br = big_button(ui, Icon::PageBreak, "Breaks", "Page Breaks", true); + for sequence in keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["pageLayout.breaks"]) { + keytip(app, ui, sequence, br.rect.center_bottom()); + } egui::Popup::menu(&br).show(|ui| { menu_items( app, @@ -1423,7 +1553,9 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { ], ); }); - if big_button(ui, Icon::Sheet, "Print\nTitles", "Print Titles", false).clicked() { + let __k = big_button(ui, Icon::Sheet, "Print\nTitles", "Print Titles", false); + control_keytip(app, ui, &__k, "pageSetup"); + if __k.clicked() { app.open_dialog("pageSetup", json!({"tab": "Sheet"})); } sep(ui); @@ -1470,10 +1602,13 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { } fn formulas(app: &mut SheetApp, ui: &mut Ui) { - if big_button(ui, Icon::Function, "Insert\nFunction", "Insert Function (⇧F3)", false).clicked() { + let __k = big_button(ui, Icon::Function, "Insert\nFunction", "Insert Function (⇧F3)", false); + control_keytip(app, ui, &__k, "insertFunction"); + if __k.clicked() { app.open_dialog("insertFunction", json!({})); } let s = big_button(ui, Icon::Sum, "AutoSum", "AutoSum", true); + control_keytip(app, ui, &s, "formulas.autoSum"); if s.clicked() { act(app, "formulas.autoSum", json!({})); } @@ -1508,14 +1643,19 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { }); } sep(ui); - if big_button(ui, Icon::Name, "Name\nManager", "Name Manager", false).clicked() { + let __k = big_button(ui, Icon::Name, "Name\nManager", "Name Manager", false); + control_keytip(app, ui, &__k, "nameManager"); + if __k.clicked() { app.open_dialog("nameManager", json!({})); } ui.vertical(|ui| { - if small_button(ui, Icon::Name, "Define Name", "Define Name", false).clicked() { + let __k = small_button(ui, Icon::Name, "Define Name", "Define Name", false); + control_keytip(app, ui, &__k, "defineName"); + if __k.clicked() { app.open_dialog("defineName", json!({})); } let use_in = small_button(ui, Icon::Function, "Use in Formula", "Use in Formula", true); + anchor_keytip(app, ui, &use_in, &["edit.pasteSpecial"]); egui::Popup::menu(&use_in).show(|ui| { let names: Vec = app.session.active().map(|d| d.wb.names.iter().map(|n| n.name.clone()).collect()).unwrap_or_default(); for n in names { @@ -1525,7 +1665,9 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { } } }); - if small_button(ui, Icon::Table, "Create from Selection", "Create from Selection", false).clicked() { + let __k = small_button(ui, Icon::Table, "Create from Selection", "Create from Selection", false); + control_keytip(app, ui, &__k, "formulas.createFromSelection"); + if __k.clicked() { act(app, "formulas.createFromSelection", json!({})); } }); @@ -1546,13 +1688,19 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { } }); ui.vertical(|ui| { - if small_button(ui, Icon::Fx, "Show Formulas", "Show Formulas (⌃`)", false).clicked() { + let __k = small_button(ui, Icon::Fx, "Show Formulas", "Show Formulas (⌃`)", false); + control_keytip(app, ui, &__k, "formulas.showFormulas"); + if __k.clicked() { act(app, "formulas.showFormulas", json!({})); } - if small_button(ui, Icon::Validation, "Error Checking", "Error Checking", false).clicked() { + let __k = small_button(ui, Icon::Validation, "Error Checking", "Error Checking", false); + control_keytip(app, ui, &__k, "errorChecking"); + if __k.clicked() { app.open_dialog("errorChecking", json!({})); } - if small_button(ui, Icon::Calc, "Evaluate Formula", "Evaluate Formula", false).clicked() { + let __k = small_button(ui, Icon::Calc, "Evaluate Formula", "Evaluate Formula", false); + control_keytip(app, ui, &__k, "evaluateFormula"); + if __k.clicked() { app.open_dialog("evaluateFormula", json!({})); } }); @@ -1561,6 +1709,9 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { } sep(ui); let co = big_button(ui, Icon::Calc, "Calculation\nOptions", "Calculation Options", true); + for sequence in keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["formulas.calculationOptions"]) { + keytip(app, ui, sequence, co.rect.center_bottom()); + } egui::Popup::menu(&co).show(|ui| { menu_items( app, @@ -1573,10 +1724,14 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { ); }); ui.vertical(|ui| { - if small_button(ui, Icon::Calc, "Calculate Now", "Calculate Now (F9)", false).clicked() { + let __k = small_button(ui, Icon::Calc, "Calculate Now", "Calculate Now (F9)", false); + control_keytip(app, ui, &__k, "formulas.calculateNow"); + if __k.clicked() { act(app, "formulas.calculateNow", json!({})); } - if small_button(ui, Icon::Sheet, "Calculate Sheet", "Calculate Sheet (⇧F9)", false).clicked() { + let __k = small_button(ui, Icon::Sheet, "Calculate Sheet", "Calculate Sheet (⇧F9)", false); + control_keytip(app, ui, &__k, "formulas.calculateSheet"); + if __k.clicked() { act(app, "formulas.calculateSheet", json!({})); } }); @@ -1584,43 +1739,67 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { fn data(app: &mut SheetApp, ui: &mut Ui) { let gd = big_button(ui, Icon::Folder, "Get Data\n(Text/CSV)", "Import a text or CSV file", false); + control_keytip(app, ui, &gd, "data.getData"); if gd.clicked() { app.open_dialog("open", json!({})); } sep(ui); ui.vertical(|ui| { - if small_button(ui, Icon::SortAsc, "", "Sort A to Z", false).clicked() { + let __k = small_button(ui, Icon::SortAsc, "", "Sort A to Z", false); + control_keytip(app, ui, &__k, "data.sortAscending"); + if __k.clicked() { act(app, "data.sortAscending", json!({})); } - if small_button(ui, Icon::SortDesc, "", "Sort Z to A", false).clicked() { + let __k = small_button(ui, Icon::SortDesc, "", "Sort Z to A", false); + control_keytip(app, ui, &__k, "data.sortDescending"); + if __k.clicked() { act(app, "data.sortDescending", json!({})); } }); - if big_button(ui, Icon::SortFilter, "Sort", "Custom Sort", false).clicked() { + let __k = big_button(ui, Icon::SortFilter, "Sort", "Custom Sort", false); + control_keytip(app, ui, &__k, "sort"); + if __k.clicked() { app.open_dialog("sort", json!({})); } - if big_button(ui, Icon::Filter, "Filter", "Filter (⌘⇧F)", false).clicked() { + let __k = big_button(ui, Icon::Filter, "Filter", "Filter (⌘⇧F)", false); + control_keytip(app, ui, &__k, "data.filter"); + if __k.clicked() { act(app, "data.filter", json!({})); } ui.vertical(|ui| { - if small_button(ui, Icon::Eraser, "Clear", "Clear Filter", false).clicked() { + let __k = small_button(ui, Icon::Eraser, "Clear", "Clear Filter", false); + control_keytip(app, ui, &__k, "data.clearFilter"); + if __k.clicked() { act(app, "data.clearFilter", json!({})); } - if small_button(ui, Icon::Redo, "Reapply", "Reapply", false).clicked() { + let __k = small_button(ui, Icon::Redo, "Reapply", "Reapply", false); + control_keytip(app, ui, &__k, "data.reapply"); + if __k.clicked() { act(app, "data.reapply", json!({})); } }); sep(ui); - if big_button(ui, Icon::TextColumns, "Text to\nColumns", "Text to Columns", false).clicked() { + let __k = big_button(ui, Icon::TextColumns, "Text to\nColumns", "Text to Columns", false); + control_keytip(app, ui, &__k, "textToColumns"); + if __k.clicked() { app.open_dialog("textToColumns", json!({})); } - if big_button(ui, Icon::FillDown, "Flash\nFill", "Flash Fill (⌘E)", false).clicked() { + let __k = big_button(ui, Icon::FillDown, "Flash\nFill", "Flash Fill (⌘E)", false); + control_keytip(app, ui, &__k, "edit.flashFill"); + if __k.clicked() { act(app, "edit.flashFill", json!({})); } - if big_button(ui, Icon::Duplicates, "Remove\nDuplicates", "Remove Duplicates", false).clicked() { + let __k = big_button(ui, Icon::Duplicates, "Remove\nDuplicates", "Remove Duplicates", false); + control_keytip(app, ui, &__k, "removeDuplicates"); + if __k.clicked() { app.open_dialog("removeDuplicates", json!({})); } let dv = big_button(ui, Icon::Validation, "Data\nValidation", "Data Validation", true); + for sequence in + keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["dialog:dataValidation", "data.circleInvalid", "data.validation"]) + { + keytip(app, ui, sequence, dv.rect.center_bottom()); + } egui::Popup::menu(&dv).show(|ui| { menu_items( app, @@ -1634,6 +1813,11 @@ fn data(app: &mut SheetApp, ui: &mut Ui) { }); sep(ui); let wi = big_button(ui, Icon::Calc, "What-If\nAnalysis", "What-If Analysis", true); + for sequence in + keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["dialog:goalSeek", "data.scenarioManager", "data.dataTable"]) + { + keytip(app, ui, sequence, wi.rect.center_bottom()); + } egui::Popup::menu(&wi).show(|ui| { menu_items( app, @@ -1650,13 +1834,19 @@ fn data(app: &mut SheetApp, ui: &mut Ui) { } sep(ui); ui.vertical(|ui| { - if small_button(ui, Icon::Group, "Group", "Group", false).clicked() { + let __k = small_button(ui, Icon::Group, "Group", "Group", false); + control_keytip(app, ui, &__k, "data.group"); + if __k.clicked() { act(app, "data.group", json!({})); } - if small_button(ui, Icon::Ungroup, "Ungroup", "Ungroup", false).clicked() { + let __k = small_button(ui, Icon::Ungroup, "Ungroup", "Ungroup", false); + control_keytip(app, ui, &__k, "data.ungroup"); + if __k.clicked() { act(app, "data.ungroup", json!({})); } - if small_button(ui, Icon::Subtotal, "Subtotal", "Subtotal", false).clicked() { + let __k = small_button(ui, Icon::Subtotal, "Subtotal", "Subtotal", false); + control_keytip(app, ui, &__k, "subtotal"); + if __k.clicked() { app.open_dialog("subtotal", json!({})); } }); @@ -1679,20 +1869,33 @@ fn data(app: &mut SheetApp, ui: &mut Ui) { } fn review(app: &mut SheetApp, ui: &mut Ui) { - if big_button(ui, Icon::Spell, "Spelling", "Spelling (F7)", false).clicked() { + let __k = big_button(ui, Icon::Spell, "Spelling", "Spelling (F7)", false); + control_keytip(app, ui, &__k, "spelling"); + if __k.clicked() { app.open_dialog("spelling", json!({})); } - if big_button(ui, Icon::Calc, "Workbook\nStatistics", "Workbook Statistics", false).clicked() { + let __k = big_button(ui, Icon::Calc, "Workbook\nStatistics", "Workbook Statistics", false); + control_keytip(app, ui, &__k, "statistics"); + if __k.clicked() { app.open_dialog("statistics", json!({})); } - if big_button(ui, Icon::Validation, "Check\nAccessibility", "Check Accessibility", false).clicked() { + let __k = big_button(ui, Icon::Validation, "Check\nAccessibility", "Check Accessibility", false); + control_keytip(app, ui, &__k, "accessibility"); + if __k.clicked() { app.open_dialog("accessibility", json!({})); } sep(ui); - if big_button(ui, Icon::Comment, "New\nComment", "New Comment", false).clicked() { + let __k = big_button(ui, Icon::Comment, "New\nComment", "New Comment", false); + control_keytip(app, ui, &__k, "comment"); + if __k.clicked() { app.open_dialog("comment", json!({"threaded": true})); } - if big_button(ui, Icon::Delete, "Delete", "Delete Comment", false).clicked() { + if big_button(ui, Icon::Comment, "Show\nComments", "Comments pane", false).clicked() { + app.grid.pane = Some("comments".into()); + } + let __k = big_button(ui, Icon::Delete, "Delete", "Delete Comment", false); + control_keytip(app, ui, &__k, "review.deleteComment"); + if __k.clicked() { act(app, "review.deleteComment", json!({})); } ui.vertical(|ui| { @@ -1717,6 +1920,7 @@ fn review(app: &mut SheetApp, ui: &mut Ui) { } } let notes = big_button(ui, Icon::Note, "Notes", "Notes", true); + anchor_keytip(app, ui, ¬es, &["review.newNote"]); egui::Popup::menu(¬es).show(|ui| { menu_items( app, @@ -1732,14 +1936,18 @@ fn review(app: &mut SheetApp, ui: &mut Ui) { }); sep(ui); let protected = app.session.active().and_then(|d| d.wb.active().map(|s| s.is_protected())).unwrap_or(false); - if big_button(ui, Icon::Lock, if protected { "Unprotect\nSheet" } else { "Protect\nSheet" }, "Protect Sheet", false).clicked() { + let __k = big_button(ui, Icon::Lock, if protected { "Unprotect\nSheet" } else { "Protect\nSheet" }, "Protect Sheet", false); + control_keytip(app, ui, &__k, "review.protectSheet"); + if __k.clicked() { if protected { app.open_dialog("unprotectSheet", json!({})); } else { app.open_dialog("protectSheet", json!({})); } } - if big_button(ui, Icon::Book, "Protect\nWorkbook", "Protect Workbook structure", false).clicked() { + let __k = big_button(ui, Icon::Book, "Protect\nWorkbook", "Protect Workbook structure", false); + control_keytip(app, ui, &__k, "review.protectWorkbook"); + if __k.clicked() { crate::backstage::protect_workbook(app); } if big_button(ui, Icon::Validation, "Allow Edit\nRanges", "Allow Edit Ranges", false).clicked() { @@ -1784,18 +1992,30 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { } }); sep(ui); - if big_button(ui, Icon::Zoom, "Zoom", "Zoom…", false).clicked() { + let __k = big_button(ui, Icon::Zoom, "Zoom", "Zoom…", false); + control_keytip(app, ui, &__k, "zoom"); + if __k.clicked() { app.open_dialog("zoom", json!({})); } - if big_button(ui, Icon::Search, "100%", "Zoom to 100%", false).clicked() { - act(app, "view.zoom100", json!({})); + let __k = big_button(ui, Icon::Search, "100%", "Zoom to 100%", false); + control_keytip(app, ui, &__k, "view.zoom"); + if __k.clicked() { + act(app, "view.zoom", json!({"percent": 100})); } - if big_button(ui, Icon::Zoom, "Zoom to\nSelection", "Zoom to Selection", false).clicked() { + let __k = big_button(ui, Icon::Zoom, "Zoom to\nSelection", "Zoom to Selection", false); + control_keytip(app, ui, &__k, "view.zoomToSelection"); + if __k.clicked() { let r = app.grid.cells_rect.unwrap_or(Rect::from_min_size(pos2(0.0, 0.0), vec2(1200.0, 700.0))); act(app, "view.zoomToSelection", json!({"viewWidth": r.width(), "viewHeight": r.height()})); } sep(ui); let fp = big_button(ui, Icon::Freeze, "Freeze\nPanes", "Freeze Panes", true); + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &["view.freezePanes", "view.freezeTopRow", "view.freezeFirstColumn", "view.unfreezePanes"], + ) { + keytip(app, ui, sequence, fp.rect.center_bottom()); + } egui::Popup::menu(&fp).show(|ui| { menu_items( app, @@ -1867,16 +2087,24 @@ fn automate(app: &mut SheetApp, ui: &mut Ui) { if big_button(ui, Icon::Script, "Macros", "Macros and scripts in this workbook (Alt+F8)", false).clicked() { app.open_dialog("macros", json!({})); } - if big_button(ui, Icon::Script, "Command\nPalette", "Search and run any command", false).clicked() { + let __k = big_button(ui, Icon::Script, "Command\nPalette", "Search and run any command", false); + control_keytip(app, ui, &__k, "commandSearch"); + if __k.clicked() { app.open_dialog("commandSearch", json!({})); } - if big_button(ui, Icon::Script, "Agent\nControl", "How agents drive GridCraft (MCP / control channel)", false).clicked() { + let __k = big_button(ui, Icon::Script, "Agent\nControl", "How agents drive GridCraft (MCP / control channel)", false); + control_keytip(app, ui, &__k, "agents"); + if __k.clicked() { app.open_dialog("agents", json!({})); } - if big_button(ui, Icon::Script, "Action\nJournal", "Every command run in this session (replayable)", false).clicked() { + let __k = big_button(ui, Icon::Script, "Action\nJournal", "Every command run in this session (replayable)", false); + control_keytip(app, ui, &__k, "journal"); + if __k.clicked() { app.open_dialog("journal", json!({})); } - if big_button(ui, Icon::Script, "About\nGridCraft", "Version, contributors and the AI models that helped", false).clicked() { + let __k = big_button(ui, Icon::Script, "About\nGridCraft", "Version, contributors and the AI models that helped", false); + control_keytip(app, ui, &__k, "about"); + if __k.clicked() { app.open_dialog("about", json!({})); } } @@ -1899,10 +2127,14 @@ fn table_design(app: &mut SheetApp, ui: &mut Ui) { } }); sep(ui); - if big_button(ui, Icon::Duplicates, "Remove\nDuplicates", "Remove Duplicates", false).clicked() { + let __k = big_button(ui, Icon::Duplicates, "Remove\nDuplicates", "Remove Duplicates", false); + control_keytip(app, ui, &__k, "data.removeDuplicates"); + if __k.clicked() { act(app, "data.removeDuplicates", json!({})); } - if big_button(ui, Icon::Table, "Convert\nto Range", "Convert to Range", false).clicked() { + let __k = big_button(ui, Icon::Table, "Convert\nto Range", "Convert to Range", false); + control_keytip(app, ui, &__k, "table.convertToRange"); + if __k.clicked() { act(app, "table.convertToRange", json!({"table": tname})); } sep(ui); @@ -1936,12 +2168,16 @@ fn table_design(app: &mut SheetApp, ui: &mut Ui) { }); sep(ui); let st = big_button(ui, Icon::FormatTable, "Table\nStyles", "Table Styles", true); + anchor_keytip(app, ui, &st, &["table.style"]); egui::Popup::menu(&st).show(|ui| table_gallery(app, ui, "table.style")); } fn chart_design(app: &mut SheetApp, ui: &mut Ui) { let Some(id) = app.selected_chart else { return }; let ae = big_button(ui, Icon::Chart, "Add Chart\nElement", "Add Chart Element", true); + for sequence in keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["chart.set"]) { + keytip(app, ui, sequence, ae.rect.center_bottom()); + } egui::Popup::menu(&ae).show(|ui| { menu_items( app, @@ -1959,13 +2195,21 @@ fn chart_design(app: &mut SheetApp, ui: &mut Ui) { ], ); }); - if big_button(ui, Icon::Redo, "Switch\nRow/Column", "Switch Row/Column", false).clicked() { + let __k = big_button(ui, Icon::Redo, "Switch\nRow/Column", "Switch Row/Column", false); + control_keytip(app, ui, &__k, "chart.switchRowColumn"); + if __k.clicked() { act(app, "chart.switchRowColumn", json!({"chart": id})); } if big_button(ui, Icon::Table, "Select\nData", "Select Data", false).clicked() { app.open_dialog("selectData", json!({"chart": id})); } let ct = big_button(ui, Icon::ChartBar, "Change\nChart Type", "Change Chart Type", true); + for sequence in keytip_table::group_extension( + app.keytips.prefix().unwrap_or_default(), + &["column", "bar", "line", "area", "pie", "doughnut", "scatter", "radar", "waterfall", "funnel", "treemap"], + ) { + keytip(app, ui, sequence, ct.rect.center_bottom()); + } egui::Popup::menu(&ct).show(|ui| { for (label, k, s) in [ ("Clustered Column", "column", ""), @@ -1989,10 +2233,14 @@ fn chart_design(app: &mut SheetApp, ui: &mut Ui) { if big_button(ui, Icon::Settings, "Format\nPane", "Format Chart pane", false).clicked() { app.grid.pane = Some("formatChart".into()); } - if big_button(ui, Icon::TextBox, "Chart\nTitle", "Edit chart title", false).clicked() { + let __k = big_button(ui, Icon::TextBox, "Chart\nTitle", "Edit chart title", false); + control_keytip(app, ui, &__k, "chartTitle"); + if __k.clicked() { app.open_dialog("chartTitle", json!({"chart": id})); } - if big_button(ui, Icon::Delete, "Delete\nChart", "Delete chart", false).clicked() { + let __k = big_button(ui, Icon::Delete, "Delete\nChart", "Delete chart", false); + control_keytip(app, ui, &__k, "chart.delete"); + if __k.clicked() { act(app, "chart.delete", json!({"chart": id})); app.selected_chart = None; } From 659a4d5bd3011781bb50016823565225c905ebea Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 20:38:31 +0300 Subject: [PATCH 07/15] Real-Excel keytip corrections from James (Home/Insert/Formulas) - HP -> home.percent (was wrongly numberFormat); HN stays the Number dropdown. - H3 -> home.underline fires directly; H3U/H3D dropped as unreachable. - HAT/HAM/HAB -> Top/Middle/Bottom Align (were unbound). - NCM/NZT/NZS -> modern Comment/Text Box/Symbol (NC2/NZTX/NU kept as legacy aliases); MI1 -> Financial first item. - Badges on the three dropdowns (font FF, font size FS, number format N) and the vertical alignment buttons; Print Titles paints PI. --- .../src/keytips/keytip_table/generated.rs | 13 ++-- crates/ui-egui/src/keytips/keytips.json | 65 +++++++++++++------ crates/ui-egui/src/ribbon.rs | 48 +++++++------- 3 files changed, 80 insertions(+), 46 deletions(-) diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index c078e77..c080cd5 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -17,6 +17,9 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("E", Entry::Tab("Home")), // Legacy Edit-menu path (Excel 2003); leads to the same Home menus. ("O", Entry::Tab("Home")), // Legacy Format-menu path (Excel 2003); leads to the same Home menus. ("HAR", Entry::Finish(Action::Command("home.alignRight"))), + ("HAT", Entry::Finish(Action::Command("home.alignTop"))), // Excel: Excel: Top Align + ("HAM", Entry::Finish(Action::Command("home.alignMiddle"))), // Excel: Excel: Middle Align + ("HAB", Entry::Finish(Action::Command("home.alignBottom"))), // Excel: Excel: Bottom Align ("HAL", Entry::Finish(Action::Command("home.alignLeft"))), ("HAC", Entry::Finish(Action::Command("home.alignCenter"))), ("HVS", Entry::Finish(Action::Dialog("pasteSpecial"))), @@ -33,9 +36,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("H0", Entry::Finish(Action::Command("home.increaseDecimal"))), // Excel: Increase Decimals; containment(Increase Decimal) ("H1", Entry::Finish(Action::Command("home.bold"))), // Excel: Bold ("H2", Entry::Finish(Action::Command("home.italic"))), // Excel: Italic - ("H3", Entry::Continue), // Excel: Underline Menu; group level: - ("H3D", Entry::Finish(Action::Command("home.doubleUnderline"))), // Excel: Double Underline - ("H3U", Entry::Finish(Action::Command("home.underline"))), // Excel: Underline + ("H3", Entry::Finish(Action::Command("home.underline"))), // Excel: Excel: Underline; fires immediately (not a menu) ("H5", Entry::Finish(Action::Command("home.decreaseIndent"))), // Excel: Decrease Indent ("H6", Entry::Finish(Action::Command("home.increaseIndent"))), // Excel: Increase Indent ("H9", Entry::Finish(Action::Command("home.decreaseDecimal"))), // Excel: Decrease Decimals; containment(Decrease Decimal) @@ -140,7 +141,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HOUR", Entry::Finish(Action::Command("home.hideRows"))), // Excel: Hide Rows ("HOUS", Entry::Finish(Action::Command("sheet.hide"))), // Excel: Hide Sheet ("HOW", Entry::Finish(Action::Command("home.columnWidth"))), // Excel: Column Width - ("HP", Entry::Finish(Action::Command("home.numberFormat"))), // Excel: Percentage Style; format TBD + ("HP", Entry::Finish(Action::Command("home.percent"))), // Excel: Excel: Percentage Style ("HSC", Entry::Finish(Action::Command("data.clearFilter"))), // Excel: Clear Filter ("HSF", Entry::Finish(Action::Command("data.filter"))), // Excel: Filter ("HSO", Entry::Finish(Action::Command("data.sortDescending"))), // Excel: Sort Z to A @@ -176,6 +177,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HX", Entry::Finish(Action::Command("edit.cut"))), // Excel: Cut ("NC1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Column Chart; Column chart (kind TBD) ("NC2", Entry::Finish(Action::Command("review.newComment"))), // Excel: New Comment + ("NCM", Entry::Finish(Action::Command("review.newComment"))), // Excel: Excel: New Comment (modern) ("NCB", Entry::Finish(Action::Command("insert.checkbox"))), // Excel: Checkbox ("ND", Entry::Finish(Action::Command("insert.chart"))), // Excel: Scatter Chart; Scatter (kind TBD) ("NE", Entry::Finish(Action::Pending("insert.equation"))), // pending; Excel: Equations; Equations; reserved until the command lands @@ -209,6 +211,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("NSZ", Entry::Finish(Action::Pending("insert.pivotChart"))), // pending; Excel: PivotChart; PivotChart; reserved until the command lands ("NT", Entry::Finish(Action::Command("insert.table"))), // Excel: Table ("NU", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Symbols; containment(Symbol) + ("NZS", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Excel: Symbol (modern) ("NV", Entry::Finish(Action::Command("insert.pivotTable"))), // Excel: PivotTable ("NW", Entry::Finish(Action::Pending("insert.wordArt"))), // pending; Excel: WordArt; WordArt; reserved until the command lands ("NX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box @@ -217,6 +220,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("NZKSO", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Column Sparkline ("NZKSW", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Win/Loss Sparkline ("NZTX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box + ("NZT", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Excel: Text Box (modern) ("PAE", Entry::Finish(Action::Command("arrange.sendBackward"))), // Excel: Send Backward ("PAF", Entry::Finish(Action::Command("arrange.bringForward"))), // Excel: Bring Forward ("PB", Entry::Continue), // Excel: Breaks; group level: @@ -253,6 +257,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("MGF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Math & Trig - Insert Function; containment(Insert Function) ("MH", Entry::Finish(Action::Command("formulas.showFormulas"))), // Excel: Show Formulas ("MI", Entry::Continue), // Excel: Financial Menu; group level: containment(Financial) + ("MI1", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Excel: Financial; first item of the dropdown ("MIF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Financial - Insert Function; containment(Insert Function) ("MJ", Entry::Finish(Action::Command("formulas.calculateSheet"))), // Excel: Calculate Sheet ("MK", Entry::Continue), // Excel: Error Checking Menu; group level: containment(Error Checking) diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index 1056d31..b3545b6 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -69,6 +69,24 @@ "kind": "command", "target": "home.alignRight" }, + { + "seq": "HAT", + "kind": "command", + "target": "home.alignTop", + "excel": "Excel: Top Align" + }, + { + "seq": "HAM", + "kind": "command", + "target": "home.alignMiddle", + "excel": "Excel: Middle Align" + }, + { + "seq": "HAB", + "kind": "command", + "target": "home.alignBottom", + "excel": "Excel: Bottom Align" + }, { "seq": "HAL", "kind": "command", @@ -177,23 +195,9 @@ }, { "seq": "H3", - "kind": "continue", - "excel": "Underline Menu", - "note": "group level:" - }, - { - "seq": "H3D", "kind": "command", - "target": "home.doubleUnderline", - "excel": "Double Underline", - "note": "" - }, - { - "seq": "H3U", - "kind": "command", - "target": "home.underline", - "excel": "Underline", - "note": "" + "excel": "Excel: Underline; fires immediately (not a menu)", + "target": "home.underline" }, { "seq": "H5", @@ -924,9 +928,8 @@ { "seq": "HP", "kind": "command", - "target": "home.numberFormat", - "excel": "Percentage Style", - "note": "format TBD" + "target": "home.percent", + "excel": "Excel: Percentage Style" }, { "seq": "HSC", @@ -1170,6 +1173,12 @@ "excel": "New Comment", "note": "" }, + { + "seq": "NCM", + "kind": "command", + "target": "review.newComment", + "excel": "Excel: New Comment (modern)" + }, { "seq": "NCB", "kind": "command", @@ -1404,6 +1413,12 @@ "excel": "Symbols", "note": "containment(Symbol)" }, + { + "seq": "NZS", + "kind": "command", + "target": "insert.symbol", + "excel": "Excel: Symbol (modern)" + }, { "seq": "NV", "kind": "command", @@ -1460,6 +1475,12 @@ "excel": "Text Box", "note": "" }, + { + "seq": "NZT", + "kind": "command", + "target": "insert.textBox", + "excel": "Excel: Text Box (modern)" + }, { "seq": "PAE", "kind": "command", @@ -1706,6 +1727,12 @@ "excel": "Financial Menu", "note": "group level: containment(Financial)" }, + { + "seq": "MI1", + "kind": "command", + "target": "formulas.insertFunction", + "excel": "Excel: Financial; first item of the dropdown" + }, { "seq": "MIF", "kind": "command", diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 2b80957..0afc33a 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -570,16 +570,9 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { (Icon::AlignMiddle, VAlign::Center, "home.alignMiddle", "Middle Align"), (Icon::AlignBottom, VAlign::Bottom, "home.alignBottom", "Bottom Align"), ] { - let response = toggle_button(ui, icon, st.align.v == v, tip); - if matches!(app.keytips.prefix(), Some("H" | "HA")) { - let sequence = match v { - VAlign::Top => "HAT", - VAlign::Center => "HAM", - _ => "HAB", - }; - keytip(app, ui, sequence, response.rect.center_bottom()); - } - if response.clicked() { + let __k = toggle_button(ui, icon, st.align.v == v, tip); + control_keytip(app, ui, &__k, id); + if __k.clicked() { act(app, id, json!({})); } } @@ -936,12 +929,16 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { fn font_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { let mut name = st.font.name.clone(); let before = name.clone(); - egui::ComboBox::from_id_salt("font_name").width(150.0).selected_text(egui::RichText::new(&name).font(theme::ui_font(12.5))).show_ui(ui, |ui| { - for f in crate::fonts::families() { - let fam = theme::cell_family(&f, false, false); - ui.selectable_value(&mut name, f.clone(), egui::RichText::new(&f).font(egui::FontId::new(14.0, fam))); - } - }); + let resp = egui::ComboBox::from_id_salt("font_name").width(150.0).selected_text(egui::RichText::new(&name).font(theme::ui_font(12.5))).show_ui( + ui, + |ui| { + for f in FONTS { + let fam = theme::cell_family(f, false, false); + ui.selectable_value(&mut name, f.to_string(), egui::RichText::new(*f).font(egui::FontId::new(14.0, fam))); + } + }, + ); + control_keytip(app, ui, &resp.response, "home.fontName"); if name != before { act(app, "home.fontName", json!({"name": name})); } @@ -971,11 +968,15 @@ fn size_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { let mut size = st.font.size; let before = size; let label = if size.fract() == 0.0 { format!("{}", size as i32) } else { format!("{size}") }; - egui::ComboBox::from_id_salt("font_size").width(52.0).selected_text(egui::RichText::new(label).font(theme::ui_font(12.5))).show_ui(ui, |ui| { - for s in [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0, 48.0, 72.0] { - ui.selectable_value(&mut size, s, format!("{}", s as i32)); - } - }); + let resp = egui::ComboBox::from_id_salt("font_size").width(52.0).selected_text(egui::RichText::new(label).font(theme::ui_font(12.5))).show_ui( + ui, + |ui| { + for s in [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0, 48.0, 72.0] { + ui.selectable_value(&mut size, s, format!("{}", s as i32)); + } + }, + ); + control_keytip(app, ui, &resp.response, "home.fontSize"); if size != before { act(app, "home.fontSize", json!({"size": size})); } @@ -988,7 +989,7 @@ fn number_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { let lang = app.ui.language; let sample = app.session.active().and_then(|d| d.wb.active().map(|sh| sh.value(d.selection.active))).unwrap_or_default(); let mut picked: Option<&str> = None; - egui::ComboBox::from_id_salt("number_format") + let resp = egui::ComboBox::from_id_salt("number_format") .width(150.0) .selected_text(egui::RichText::new(lang.tr(¤t)).font(theme::ui_font(12.5))) .show_ui(ui, |ui| { @@ -1007,6 +1008,7 @@ fn number_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { picked = Some("__more"); } }); + anchor_keytip(app, ui, &resp.response, &["home.numberFormat"]); match picked { Some("__more") => app.open_dialog("formatCells", json!({"tab": "Number"})), Some(n) => act(app, "home.numberFormat", json!({"format": n})), @@ -1554,7 +1556,7 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { ); }); let __k = big_button(ui, Icon::Sheet, "Print\nTitles", "Print Titles", false); - control_keytip(app, ui, &__k, "pageSetup"); + control_keytip(app, ui, &__k, "pageLayout.printTitles"); if __k.clicked() { app.open_dialog("pageSetup", json!({"tab": "Sheet"})); } From cd5f3f0a7169b69ec4de886f87825884c55947fe Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 20:53:21 +0300 Subject: [PATCH 08/15] T6: pending keytips resolve at runtime and go live when their command lands - Action::Pending now consults the engine catalog at fire time: silent no-op while the command is missing, runs it once it exists - no table edit needed when a future feature lands. - Stripped stray backticks from 7 pending target ids so catalog lookup can ever match them. - gridcraft-engine: doc(hidden) register_test_command hook so UI tests can prove the go-live path without touching production code. - Tests: pending_sequences_are_silent_no_ops (no dialog/toast/journal), pending_sequences_go_live_when_the_command_lands (fake command fires via its reserved sequence), every_sequence_terminates_without_panicking (all 413 sequences pressed end-to-end). --- crates/engine/src/cmd/mod.rs | 37 ++++++++++++- crates/engine/src/lib.rs | 2 +- .../src/keytips/keytip_table/generated.rs | 52 +++++++++---------- crates/ui-egui/src/keytips/keytips.json | 14 ++--- crates/ui-egui/src/lib.rs | 10 ++-- crates/ui-egui/tests/keytips.rs | 46 ++++++++++++++++ 6 files changed, 123 insertions(+), 38 deletions(-) diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 7a380c7..ef90a6c 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -149,7 +149,42 @@ pub fn command_specs() -> &'static [CommandSpec] { } pub fn find_command(id: &str) -> Option<&'static CommandSpec> { - command_specs().iter().find(|c| c.id == id) + if let Some(spec) = command_specs().iter().find(|c| c.id == id) { + return Some(spec); + } + // Commands registered by UI integration tests (pending keytips go live + // the moment their command lands, with no table edit). + test_commands().iter().find(|c| c.id == id).copied() +} + +static TEST_COMMANDS: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); + +/// Test-only: register an extra command so UI integration tests can prove a pending +/// keytip becomes live the moment its command lands (see crates/ui-egui/tests/keytips.rs). +/// Not part of the public API; harmless no-op for production callers who never call it. +#[doc(hidden)] +pub fn register_test_command(id: &'static str, run: Run) { + // Leaked intentionally: test commands live for the whole process. + let spec: &'static CommandSpec = Box::leak(Box::new(CommandSpec { + id, + label: id, + menu: &[], + shortcut: None, + params: "", + enabled: |_| Ok(()), + run, + journal: true, + undoable: false, + })); + test_commands().push(spec); +} + +fn test_commands() -> std::sync::MutexGuard<'static, Vec<&'static CommandSpec>> { + #[allow(clippy::unwrap_used)] // a poisoned test registry has no meaningful recovery + match TEST_COMMANDS.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } } // ---------------------------------------------------------------- param helpers diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 857a13a..3aceab0 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -29,7 +29,7 @@ use gridcraft_model::Workbook; use serde::Serialize; use serde_json::Value as Json; -pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command}; +pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command, register_test_command}; pub use gridcraft_calc as calc; pub use gridcraft_core as core; pub use gridcraft_formula as formula; diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index c080cd5..384a8dd 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -189,7 +189,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("NHI", Entry::Finish(Action::Command("insert.chart"))), // Excel: Hierarchy Chart; Hierarchy (kind TBD) ("NI1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Waterfall Chart; Waterfall (kind TBD) ("NI2", Entry::Finish(Action::Command("insert.link"))), // Excel: Link - ("NJ", Entry::Finish(Action::Pending("`insert.object`"))), // pending; Excel: Object; insert.object missing; reserved until the command lands + ("NJ", Entry::Finish(Action::Pending("insert.object"))), // pending; Excel: Object; insert.object missing; reserved until the command lands ("NM1T", Entry::Finish(Action::Command("insert.picture"))), // Excel: SmartArt Picture; containment(Picture) ("NM2", Entry::Finish(Action::Command("insert.chart"))), // Excel: Map Chart; Map (kind TBD) ("NN1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Line Chart; Line chart (kind TBD) @@ -204,47 +204,47 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("NSA", Entry::Finish(Action::Command("insert.chart"))), // Excel: Statistical Chart; Statistical chart (kind TBD) ("NSC", Entry::Finish(Action::Pending("insert.screenshot"))), // pending; Excel: Screenshot Menu; Screenshot; reserved until the command lands ("NSD", Entry::Finish(Action::Command("insert.chart"))), // Excel: Combo Chart; Combo (kind TBD) - ("NSF", Entry::Finish(Action::Pending("`insert.slicer`"))), // pending; Excel: Slicer; insert.slicer missing (gap #8); reserved until the command lands - ("NSH", Entry::Finish(Action::Command("insert.shape"))), // Excel: Shapes + ("NSF", Entry::Finish(Action::Pending("insert.slicer"))), // pending; Excel: Slicer; insert.slicer missing (gap #8); reserved until the command lands + ("NSH", Entry::Finish(Action::Command("insert.shape"))), // Excel: Shapes ("NSP", Entry::Finish(Action::Command("insert.recommendedPivotTables"))), // Excel: Recommended PivotTables - ("NST", Entry::Finish(Action::Pending("`insert.timeline`"))), // pending; Excel: Timeline; insert.timeline missing (gap #8); reserved until the command lands + ("NST", Entry::Finish(Action::Pending("insert.timeline"))), // pending; Excel: Timeline; insert.timeline missing (gap #8); reserved until the command lands ("NSZ", Entry::Finish(Action::Pending("insert.pivotChart"))), // pending; Excel: PivotChart; PivotChart; reserved until the command lands - ("NT", Entry::Finish(Action::Command("insert.table"))), // Excel: Table - ("NU", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Symbols; containment(Symbol) - ("NZS", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Excel: Symbol (modern) - ("NV", Entry::Finish(Action::Command("insert.pivotTable"))), // Excel: PivotTable - ("NW", Entry::Finish(Action::Pending("insert.wordArt"))), // pending; Excel: WordArt; WordArt; reserved until the command lands - ("NX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box - ("NZK", Entry::Continue), // Excel: Sparklines Menu; group level: + ("NT", Entry::Finish(Action::Command("insert.table"))), // Excel: Table + ("NU", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Symbols; containment(Symbol) + ("NZS", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Excel: Symbol (modern) + ("NV", Entry::Finish(Action::Command("insert.pivotTable"))), // Excel: PivotTable + ("NW", Entry::Finish(Action::Pending("insert.wordArt"))), // pending; Excel: WordArt; WordArt; reserved until the command lands + ("NX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box + ("NZK", Entry::Continue), // Excel: Sparklines Menu; group level: ("NZKSL", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Line Sparkline ("NZKSO", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Column Sparkline ("NZKSW", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Win/Loss Sparkline - ("NZTX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box - ("NZT", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Excel: Text Box (modern) + ("NZTX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box + ("NZT", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Excel: Text Box (modern) ("PAE", Entry::Finish(Action::Command("arrange.sendBackward"))), // Excel: Send Backward ("PAF", Entry::Finish(Action::Command("arrange.bringForward"))), // Excel: Bring Forward - ("PB", Entry::Continue), // Excel: Breaks; group level: + ("PB", Entry::Continue), // Excel: Breaks; group level: ("PBI", Entry::Finish(Action::Command("pageLayout.breaks"))), // Excel: Insert Page Break; Insert Page Break ("PBR", Entry::Finish(Action::Command("pageLayout.breaks"))), // Excel: Remove Page Break; Remove Page Break ("PG", Entry::Finish(Action::Command("pageLayout.background"))), // Excel: Background ("PI", Entry::Finish(Action::Command("pageLayout.printTitles"))), // Excel: Print Titles - ("PM", Entry::Continue), // Excel: Margins; group level: + ("PM", Entry::Continue), // Excel: Margins; group level: ("PMA", Entry::Finish(Action::Command("pageLayout.margins"))), // Excel: Custom Margins; containment(Margins) ("PO", Entry::Finish(Action::Command("pageLayout.orientation"))), // Excel: Orientation ("PPG", Entry::Finish(Action::Command("pageLayout.printGridlines"))), // Excel: Print Gridlines ("PPH", Entry::Finish(Action::Command("pageLayout.printHeadings"))), // Excel: Print Headings - ("PR", Entry::Continue), // Excel: Print Area; group level: + ("PR", Entry::Continue), // Excel: Print Area; group level: ("PRS", Entry::Finish(Action::Command("pageLayout.printArea"))), // Excel: Set Print Area ("PSP", Entry::Finish(Action::Command("pageLayout.pageSetup"))), // Excel: Page Setup - ("PSZ", Entry::Finish(Action::Command("pageLayout.size"))), // Excel: Size + ("PSZ", Entry::Finish(Action::Command("pageLayout.size"))), // Excel: Size ("PTC", Entry::Finish(Action::Command("pageLayout.themeColors"))), // Excel: Theme Colors; containment(Colors) ("PTE", Entry::Finish(Action::Command("pageLayout.themeEffects"))), // Excel: Theme Effects; containment(Effects) ("PTF", Entry::Finish(Action::Command("pageLayout.themeFonts"))), // Excel: Theme Fonts; containment(Fonts) - ("PTH", Entry::Continue), // Excel: Themes; group level: + ("PTH", Entry::Continue), // Excel: Themes; group level: ("PTHB", Entry::Finish(Action::Command("pageLayout.theme"))), // Excel: Browse Themes; containment(Themes) - ("PVG", Entry::Finish(Action::Command("view.gridlines"))), // Excel: View Gridlines - ("PVH", Entry::Finish(Action::Command("view.headings"))), // Excel: View Headings - ("MA", Entry::Continue), // Excel: Remove Arrows Menu; group level: containment(Remove Arrows) + ("PVG", Entry::Finish(Action::Command("view.gridlines"))), // Excel: View Gridlines + ("PVH", Entry::Finish(Action::Command("view.headings"))), // Excel: View Headings + ("MA", Entry::Continue), // Excel: Remove Arrows Menu; group level: containment(Remove Arrows) ("MAA", Entry::Finish(Action::Pending("formulas.removeArrows"))), // pending; Excel: Remove All Arrows; Remove All Arrows; reserved until the command lands ("MAP", Entry::Finish(Action::Pending("formulas.removePrecedentArrows"))), // pending; Excel: Remove Precedent Arrows; Remove Precedent Arrows; reserved until the command lands ("MB", Entry::Finish(Action::Command("formulas.calculateNow"))), // Excel: Calculate Now @@ -303,9 +303,9 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("AF1C", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from Clipboard (source TBD) ("AF1P", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from File (source TBD) ("AFA", Entry::Finish(Action::Pending("data.fromAccess"))), // pending; Excel: Get External Data: From Access; gap #12 Get & Transform; reserved until the command lands - ("AFC", Entry::Finish(Action::Pending("`data.forecastSheet`"))), // pending; Excel: Forecast Sheet; IN FLIGHT: vangel76 PR #325; reserved until the command lands - ("AFE", Entry::Finish(Action::Pending("`data.analyzeData`"))), // pending; Excel: Analyze Data; AI feature GAP; reserved until the command lands - ("AFF", Entry::Finish(Action::Command("edit.flashFill"))), // Excel: Flash Fill + ("AFC", Entry::Finish(Action::Pending("data.forecastSheet"))), // pending; Excel: Forecast Sheet; IN FLIGHT: vangel76 PR #325; reserved until the command lands + ("AFE", Entry::Finish(Action::Pending("data.analyzeData"))), // pending; Excel: Analyze Data; AI feature GAP; reserved until the command lands + ("AFF", Entry::Finish(Action::Command("edit.flashFill"))), // Excel: Flash Fill ("AFO", Entry::Finish(Action::Pending("data.fromOtherSources"))), // pending; Excel: Get External Data: From Other Sources; gap #12; reserved until the command lands ("AFT", Entry::Finish(Action::Pending("data.fromText"))), // pending; Excel: Get External Data: From Text; gap #12; reserved until the command lands ("AFW", Entry::Finish(Action::Pending("data.fromWeb"))), // pending; Excel: Get External Data: From Web; reserved until the command lands @@ -358,12 +358,12 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("RC", Entry::Finish(Action::Command("review.newComment"))), // Excel: New Comment ("RD", Entry::Finish(Action::Command("review.deleteComment"))), // Excel: Delete Comment ("RG", Entry::Finish(Action::Pending("review.showChanges"))), // pending; Excel: Track Changes; reserved until the command lands - ("RL", Entry::Finish(Action::Pending("`review.translate`"))), // pending; Excel: Translate; feature gap; reserved until the command lands + ("RL", Entry::Finish(Action::Pending("review.translate"))), // pending; Excel: Translate; feature gap; reserved until the command lands ("RN", Entry::Finish(Action::Pending("review.nextComment"))), // pending; Excel: Next Comment; Next Comment; reserved until the command lands ("RO", Entry::Finish(Action::Command("review.protectWorkbook"))), // Excel: Protect and Share Workbook ("RPS", Entry::Finish(Action::Command("review.protectSheet"))), // Excel: Protect Sheet ("RPW", Entry::Finish(Action::Command("review.protectWorkbook"))), // Excel: Protect Workbook - ("RRS", Entry::Finish(Action::Pending("`review.smartLookup`"))), // pending; Excel: Smart Lookup; feature gap; reserved until the command lands + ("RRS", Entry::Finish(Action::Pending("review.smartLookup"))), // pending; Excel: Smart Lookup; feature gap; reserved until the command lands ("RS", Entry::Finish(Action::Command("review.spelling"))), // Excel: Spelling ("RTN", Entry::Finish(Action::Command("review.newNote"))), // Excel: New Note ("RV", Entry::Finish(Action::Pending("review.previousComment"))), // pending; Excel: Previous Comment; Previous Comment; reserved until the command lands diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index b3545b6..5dd9bcc 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -1258,7 +1258,7 @@ { "seq": "NJ", "kind": "command", - "target": "`insert.object`", + "target": "insert.object", "pending": true, "excel": "Object", "note": "insert.object missing; reserved until the command lands" @@ -1364,7 +1364,7 @@ { "seq": "NSF", "kind": "command", - "target": "`insert.slicer`", + "target": "insert.slicer", "pending": true, "excel": "Slicer", "note": "insert.slicer missing (gap #8); reserved until the command lands" @@ -1386,7 +1386,7 @@ { "seq": "NST", "kind": "command", - "target": "`insert.timeline`", + "target": "insert.timeline", "pending": true, "excel": "Timeline", "note": "insert.timeline missing (gap #8); reserved until the command lands" @@ -2040,7 +2040,7 @@ { "seq": "AFC", "kind": "command", - "target": "`data.forecastSheet`", + "target": "data.forecastSheet", "pending": true, "excel": "Forecast Sheet", "note": "IN FLIGHT: vangel76 PR #325; reserved until the command lands" @@ -2048,7 +2048,7 @@ { "seq": "AFE", "kind": "command", - "target": "`data.analyzeData`", + "target": "data.analyzeData", "pending": true, "excel": "Analyze Data", "note": "AI feature GAP; reserved until the command lands" @@ -2439,7 +2439,7 @@ { "seq": "RL", "kind": "command", - "target": "`review.translate`", + "target": "review.translate", "pending": true, "excel": "Translate", "note": "feature gap; reserved until the command lands" @@ -2475,7 +2475,7 @@ { "seq": "RRS", "kind": "command", - "target": "`review.smartLookup`", + "target": "review.smartLookup", "pending": true, "excel": "Smart Lookup", "note": "feature gap; reserved until the command lands" diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 90287b7..b62bebc 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1015,9 +1015,13 @@ impl SheetApp { } keytips::Action::Command(id) => self.run_or_alert(id, json!({})), keytips::Action::Dialog(name) => self.open_dialog(name, json!({})), - // Reserved sequence: the command does not exist yet. Badge shows, invocation - // is silent (see keytips.json `pending`). - keytips::Action::Pending(_) => {} + // Reserved sequence: fires silently while its command is missing. When the + // command lands in the catalog it goes live with no table edit. + keytips::Action::Pending(id) => { + if gridcraft_engine::find_command(id).is_some() { + self.run_or_alert(id, json!({})); + } + } } } } diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index 81a1ff6..35b3e1e 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -401,3 +401,49 @@ fn vertical_alignment_keytips_run_their_commands() { assert_eq!(h.state().session.journal.iter().filter(|(j, _)| j == id).count(), 1, "HA{} runs {id}", letter.name()); } } + +#[test] +fn pending_sequences_are_silent_no_ops() { + let mut h = harness(blank()); + let journal = h.state().session.journal.len(); + sequence(&mut h, &[Key::N, Key::E], false); // NE -> insert.equation (missing) + assert!(h.state().dialog.is_none(), "no dialog for a pending sequence"); + assert!(h.state().message.is_none(), "no message box for a pending sequence"); + assert!(h.state().toast.is_none(), "no toast for a pending sequence"); + assert_eq!(h.state().session.journal.len(), journal, "no journal entry for a pending sequence"); + assert_eq!(h.state().keytips.prefix(), None, "the finished sequence clears the prefix"); +} + +#[test] +fn pending_sequences_go_live_when_the_command_lands() { + // The command lands in the catalog (here: a test-registered stand-in). + // The sequence must run it with no table edit. + gridcraft_engine::register_test_command("insert.slicer", |s, _| { + s.execute("cell.set", json!({"cell": "Z9", "input": "live"}))?; + Ok(json!({})) + }); + let mut h = harness(blank()); + sequence(&mut h, &[Key::N, Key::S, Key::F], false); // NSF -> insert.slicer + assert_eq!(value(&h, "Z9"), Value::from("live"), "the pending sequence ran the landed command"); + assert_eq!( + h.state().session.journal.iter().filter(|(id, _)| id == "insert.slicer").count(), + 1, + "the landed command is journaled like any live command" + ); + assert_eq!(h.state().keytips.prefix(), None); +} + +#[test] +fn every_sequence_terminates_without_panicking() { + use gridcraft_ui_egui::keytips::keytip_table::SEQUENCES; + for (seq, _entry) in SEQUENCES { + let mut h = harness(blank()); + key(&mut h, Key::F10); // arm + for ch in seq.chars() { + let k = Key::from_name(&ch.to_string()).expect("sequence letters map to keys"); + letter(&mut h, k, false); + } + // A finish/tab/dialog/wait state is all fine; the point is nothing panics. + } + assert_eq!(SEQUENCES.len(), SEQUENCES.len(), "every sequence ran"); +} From 7591fca08639ed92fac0b4878c929e1a1001b3e3 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 21:02:06 +0300 Subject: [PATCH 09/15] T7: generated per-sequence regression tests (412 tests, catalog-probed) cargo xtask keytips now emits tests/keytips_generated.rs alongside the table: one end-to-end test per sequence, asserting exactly what its action does today. The generator probes the live engine catalog at generation time (blank workbook, empty params), so each live command asserts journaled / ran clean / surfaced its missing-params error; pending asserts silence; dialogs assert the dialog opens; tabs assert the ribbon switch; group levels assert the prefix stays armed. - pending-but-landed targets get live assertions and a generation warning (HWS home.shrinkToFit and MAA formulas.removeArrows had landed since population - flipped to live) - unreachable-sequence validation: any seq with a terminal prefix entry is rejected (HCP dropped - HC Copy fires first) - ENV_SENSITIVE list keeps the checked-in file byte-stable across machines (review.spelling uses the system dictionary) - sample_sequences_work_with_text_before_key covers IME text-first event order for one representative of each action kind - generated_test_count_matches_the_table pins the table size 518 ui-egui tests green; all 8 CI gates pass. --- .../src/keytips/keytip_table/generated.rs | 5 +- crates/ui-egui/src/keytips/keytips.json | 14 +- crates/ui-egui/tests/keytips_generated.rs | 3165 +++++++++++++++++ xtask/src/keytips.rs | 394 +- 4 files changed, 3501 insertions(+), 77 deletions(-) create mode 100644 crates/ui-egui/tests/keytips_generated.rs diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index 384a8dd..ee27773 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -63,7 +63,6 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HBW", Entry::Finish(Action::Command("home.drawBorder"))), // Excel: Draw Border ("HBY", Entry::Finish(Action::Command("home.borders"))), // Excel: Line Style; Line Style (submenu TBD) ("HC", Entry::Finish(Action::Command("edit.copy"))), // Excel: Copy; fires immediately in Excel; deeper aliases dropped as unreachable - ("HCP", Entry::Finish(Action::Pending("home.copyAsPicture"))), // pending; Excel: Copy as Picture; reserved until the command lands ("HDC", Entry::Finish(Action::Command("home.deleteColumns"))), // Excel: Delete Columns; Delete Columns ("HDD", Entry::Finish(Action::Command("home.deleteCells"))), // Excel: Delete Cells ("HDR", Entry::Finish(Action::Command("home.deleteRows"))), // Excel: Delete Rows; Delete Rows @@ -172,7 +171,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HVV", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Values; paste variant, params TBD: what=values ("HVW", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Keep Source Column Widths; paste variant, params TBD: what=TBD(col-widths) ("HW", Entry::Continue), // Excel: Wrap Text Menu; group level: containment(Wrap Text) - ("HWS", Entry::Finish(Action::Pending("home.shrinkToFit"))), // pending; Excel: Shrink to Fit; Shrink to Fit; reserved until the command lands + ("HWS", Entry::Finish(Action::Command("home.shrinkToFit"))), // Excel: Shrink to Fit; landed in the catalog; live since T7 probe ("HWW", Entry::Finish(Action::Command("home.wrapText"))), // Excel: Wrap Text ("HX", Entry::Finish(Action::Command("edit.cut"))), // Excel: Cut ("NC1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Column Chart; Column chart (kind TBD) @@ -245,7 +244,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("PVG", Entry::Finish(Action::Command("view.gridlines"))), // Excel: View Gridlines ("PVH", Entry::Finish(Action::Command("view.headings"))), // Excel: View Headings ("MA", Entry::Continue), // Excel: Remove Arrows Menu; group level: containment(Remove Arrows) - ("MAA", Entry::Finish(Action::Pending("formulas.removeArrows"))), // pending; Excel: Remove All Arrows; Remove All Arrows; reserved until the command lands + ("MAA", Entry::Finish(Action::Command("formulas.removeArrows"))), // Excel: Remove All Arrows; landed in the catalog; live since T7 probe ("MAP", Entry::Finish(Action::Pending("formulas.removePrecedentArrows"))), // pending; Excel: Remove Precedent Arrows; Remove Precedent Arrows; reserved until the command lands ("MB", Entry::Finish(Action::Command("formulas.calculateNow"))), // Excel: Calculate Now ("MC", Entry::Finish(Action::Command("formulas.createFromSelection"))), // Excel: Create From Selection diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index 5dd9bcc..555377b 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -380,14 +380,6 @@ "note": "fires immediately in Excel; deeper aliases dropped as unreachable", "target": "edit.copy" }, - { - "seq": "HCP", - "kind": "command", - "target": "home.copyAsPicture", - "pending": true, - "excel": "Copy as Picture", - "note": "reserved until the command lands" - }, { "seq": "HDC", "kind": "command", @@ -1141,9 +1133,8 @@ "seq": "HWS", "kind": "command", "target": "home.shrinkToFit", - "pending": true, "excel": "Shrink to Fit", - "note": "Shrink to Fit; reserved until the command lands" + "note": "landed in the catalog; live since T7 probe" }, { "seq": "HWW", @@ -1648,9 +1639,8 @@ "seq": "MAA", "kind": "command", "target": "formulas.removeArrows", - "pending": true, "excel": "Remove All Arrows", - "note": "Remove All Arrows; reserved until the command lands" + "note": "landed in the catalog; live since T7 probe" }, { "seq": "MAP", diff --git a/crates/ui-egui/tests/keytips_generated.rs b/crates/ui-egui/tests/keytips_generated.rs new file mode 100644 index 0000000..bcc3bdf --- /dev/null +++ b/crates/ui-egui/tests/keytips_generated.rs @@ -0,0 +1,3165 @@ +//! GENERATED from keytips.json by `cargo xtask keytips`. Edit the JSON, not this file. +//! +//! One end-to-end test per keytip sequence, through the real egui input path. +//! Assertions are generated from a probe of the live engine catalog (blank +//! workbook, empty params), so each test proves exactly what its command does +//! today: journaled, ran clean, surfaced a missing-params error, opened a +//! dialog, switched tabs, waited for more input, or stayed silent (pending). + +use egui::os::OperatingSystem; +use egui::{Event, Key, Modifiers}; +use gridcraft_engine::Session; +use gridcraft_ui_egui::SheetApp; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +fn harness(session: Session) -> Harness { + let mut h = egui_kittest::Harness::builder().with_os(OperatingSystem::Nix).with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SheetApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + h +} + +fn blank() -> Session { + let mut s = Session::new(); + s.new_workbook(); + s +} + +fn key_event(key: Key, pressed: bool, modifiers: Modifiers) -> Event { + Event::Key { key, physical_key: None, pressed, repeat: false, modifiers } +} + +fn events(h: &mut Harness, events: impl IntoIterator) { + h.input_mut().events.extend(events); + h.run_steps(2); +} + +fn key(h: &mut Harness, key: Key) { + events(h, [key_event(key, true, Modifiers::NONE), key_event(key, false, Modifiers::NONE)]); +} + +fn letter(h: &mut Harness, key: Key, text_first: bool) { + let text = Event::Text(key.name().to_ascii_lowercase()); + let pressed = key_event(key, true, Modifiers::NONE); + let pair = if text_first { [text, pressed] } else { [pressed, text] }; + events(h, pair.into_iter().chain([key_event(key, false, Modifiers::NONE)])); +} + +fn sequence(h: &mut Harness, seq: &str, text_first: bool) { + key(h, Key::F10); + for ch in seq.chars() { + let k = Key::from_name(&ch.to_string()).expect("sequence letters map to keys"); + letter(h, k, text_first); + } +} + +#[test] +fn keytip_h_() { + let mut h = harness(blank()); + sequence(&mut h, "H", false); + assert_eq!(h.state().ui.ribbon_tab, "Home", "H must switch to the Home tab"); +} + +#[test] +fn keytip_n_() { + let mut h = harness(blank()); + sequence(&mut h, "N", false); + assert_eq!(h.state().ui.ribbon_tab, "Insert", "N must switch to the Insert tab"); +} + +#[test] +fn keytip_ji_() { + let mut h = harness(blank()); + sequence(&mut h, "JI", false); + assert_eq!(h.state().ui.ribbon_tab, "Draw", "JI must switch to the Draw tab"); +} + +#[test] +fn keytip_p_() { + let mut h = harness(blank()); + sequence(&mut h, "P", false); + assert_eq!(h.state().ui.ribbon_tab, "Page Layout", "P must switch to the Page Layout tab"); +} + +#[test] +fn keytip_m_() { + let mut h = harness(blank()); + sequence(&mut h, "M", false); + assert_eq!(h.state().ui.ribbon_tab, "Formulas", "M must switch to the Formulas tab"); +} + +#[test] +fn keytip_a_() { + let mut h = harness(blank()); + sequence(&mut h, "A", false); + assert_eq!(h.state().ui.ribbon_tab, "Data", "A must switch to the Data tab"); +} + +#[test] +fn keytip_r_() { + let mut h = harness(blank()); + sequence(&mut h, "R", false); + assert_eq!(h.state().ui.ribbon_tab, "Review", "R must switch to the Review tab"); +} + +#[test] +fn keytip_w_() { + let mut h = harness(blank()); + sequence(&mut h, "W", false); + assert_eq!(h.state().ui.ribbon_tab, "View", "W must switch to the View tab"); +} + +#[test] +fn keytip_e_() { + let mut h = harness(blank()); + sequence(&mut h, "E", false); + assert_eq!(h.state().ui.ribbon_tab, "Home", "E must switch to the Home tab"); +} + +#[test] +fn keytip_o_() { + let mut h = harness(blank()); + sequence(&mut h, "O", false); + assert_eq!(h.state().ui.ribbon_tab, "Home", "O must switch to the Home tab"); +} + +#[test] +fn keytip_har_() { + let mut h = harness(blank()); + sequence(&mut h, "HAR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignRight"), "HAR must journal home.alignRight"); +} + +#[test] +fn keytip_hat_() { + let mut h = harness(blank()); + sequence(&mut h, "HAT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignTop"), "HAT must journal home.alignTop"); +} + +#[test] +fn keytip_ham_() { + let mut h = harness(blank()); + sequence(&mut h, "HAM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignMiddle"), "HAM must journal home.alignMiddle"); +} + +#[test] +fn keytip_hab_() { + let mut h = harness(blank()); + sequence(&mut h, "HAB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignBottom"), "HAB must journal home.alignBottom"); +} + +#[test] +fn keytip_hal_() { + let mut h = harness(blank()); + sequence(&mut h, "HAL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignLeft"), "HAL must journal home.alignLeft"); +} + +#[test] +fn keytip_hac_() { + let mut h = harness(blank()); + sequence(&mut h, "HAC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.alignCenter"), "HAC must journal home.alignCenter"); +} + +#[test] +fn keytip_hvs_() { + let mut h = harness(blank()); + sequence(&mut h, "HVS", false); + assert!(h.state().dialog.is_some(), "HVS must open the pasteSpecial dialog"); +} + +#[test] +fn keytip_es_() { + let mut h = harness(blank()); + sequence(&mut h, "ES", false); + assert!(h.state().dialog.is_some(), "ES must open the pasteSpecial dialog"); +} + +#[test] +fn keytip_hoi_() { + let mut h = harness(blank()); + sequence(&mut h, "HOI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.autofitColumnWidth"), "HOI must journal home.autofitColumnWidth"); +} + +#[test] +fn keytip_oca_() { + let mut h = harness(blank()); + sequence(&mut h, "OCA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.autofitColumnWidth"), "OCA must journal home.autofitColumnWidth"); +} + +#[test] +fn keytip_wvg_() { + let mut h = harness(blank()); + sequence(&mut h, "WVG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines"), "WVG must journal view.gridlines"); +} + +#[test] +fn keytip_0_() { + let mut h = harness(blank()); + sequence(&mut h, "0", false); + assert!(h.state().dialog.is_none(), "0 must not open a dialog"); + assert!(h.state().message.is_none(), "0 must not open a message box"); + assert!(h.state().toast.is_none(), "0 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "0 must not journal"); +} + +#[test] +fn keytip_1_() { + let mut h = harness(blank()); + sequence(&mut h, "1", false); + assert!(h.state().dialog.is_none(), "1 must not open a dialog"); + assert!(h.state().message.is_none(), "1 must not open a message box"); + assert!(h.state().toast.is_none(), "1 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "1 must not journal"); +} + +#[test] +fn keytip_2_() { + let mut h = harness(blank()); + sequence(&mut h, "2", false); + assert!(h.state().dialog.is_none(), "2 must not open a dialog"); + assert!(h.state().message.is_none(), "2 must not open a message box"); + assert!(h.state().toast.is_none(), "2 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "2 must not journal"); +} + +#[test] +fn keytip_3_() { + let mut h = harness(blank()); + sequence(&mut h, "3", false); + assert!(h.state().dialog.is_none(), "3 must not open a dialog"); + assert!(h.state().message.is_none(), "3 must not open a message box"); + assert!(h.state().toast.is_none(), "3 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "3 must not journal"); +} + +#[test] +fn keytip_4_() { + let mut h = harness(blank()); + sequence(&mut h, "4", false); + assert!(h.state().dialog.is_none(), "4 must not open a dialog"); + assert!(h.state().message.is_none(), "4 must not open a message box"); + assert!(h.state().toast.is_none(), "4 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "4 must not journal"); +} + +#[test] +fn keytip_5_() { + let mut h = harness(blank()); + sequence(&mut h, "5", false); + assert!(h.state().dialog.is_none(), "5 must not open a dialog"); + assert!(h.state().message.is_none(), "5 must not open a message box"); + assert!(h.state().toast.is_none(), "5 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "5 must not journal"); +} + +#[test] +fn keytip_h0_() { + let mut h = harness(blank()); + sequence(&mut h, "H0", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.increaseDecimal"), "H0 must journal home.increaseDecimal"); +} + +#[test] +fn keytip_h1_() { + let mut h = harness(blank()); + sequence(&mut h, "H1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.bold"), "H1 must journal home.bold"); +} + +#[test] +fn keytip_h2_() { + let mut h = harness(blank()); + sequence(&mut h, "H2", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.italic"), "H2 must journal home.italic"); +} + +#[test] +fn keytip_h3_() { + let mut h = harness(blank()); + sequence(&mut h, "H3", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.underline"), "H3 must journal home.underline"); +} + +#[test] +fn keytip_h5_() { + let mut h = harness(blank()); + sequence(&mut h, "H5", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.decreaseIndent"), "H5 must journal home.decreaseIndent"); +} + +#[test] +fn keytip_h6_() { + let mut h = harness(blank()); + sequence(&mut h, "H6", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.increaseIndent"), "H6 must journal home.increaseIndent"); +} + +#[test] +fn keytip_h9_() { + let mut h = harness(blank()); + sequence(&mut h, "H9", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.decreaseDecimal"), "H9 must journal home.decreaseDecimal"); +} + +#[test] +fn keytip_han_() { + let mut h = harness(blank()); + sequence(&mut h, "HAN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.accounting"), "HAN must journal home.accounting"); +} + +#[test] +fn keytip_has_() { + let mut h = harness(blank()); + sequence(&mut h, "HAS", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HAS must run home.addIns cleanly"); +} + +#[test] +fn keytip_hb_() { + let mut h = harness(blank()); + sequence(&mut h, "HB", false); + assert_eq!(h.state().keytips.prefix(), Some("HB"), "HB must stay armed waiting for more input"); +} + +#[test] +fn keytip_hba_() { + let mut h = harness(blank()); + sequence(&mut h, "HBA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBA must journal home.borders"); +} + +#[test] +fn keytip_hbb_() { + let mut h = harness(blank()); + sequence(&mut h, "HBB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBB must journal home.borders"); +} + +#[test] +fn keytip_hbc_() { + let mut h = harness(blank()); + sequence(&mut h, "HBC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBC must journal home.borders"); +} + +#[test] +fn keytip_hbd_() { + let mut h = harness(blank()); + sequence(&mut h, "HBD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBD must journal home.borders"); +} + +#[test] +fn keytip_hbe_() { + let mut h = harness(blank()); + sequence(&mut h, "HBE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBE must journal home.borders"); +} + +#[test] +fn keytip_hbg_() { + let mut h = harness(blank()); + sequence(&mut h, "HBG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.drawBorder"), "HBG must journal home.drawBorder"); +} + +#[test] +fn keytip_hbh_() { + let mut h = harness(blank()); + sequence(&mut h, "HBH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBH must journal home.borders"); +} + +#[test] +fn keytip_hbi_() { + let mut h = harness(blank()); + sequence(&mut h, "HBI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBI must journal home.borders"); +} + +#[test] +fn keytip_hbl_() { + let mut h = harness(blank()); + sequence(&mut h, "HBL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBL must journal home.borders"); +} + +#[test] +fn keytip_hbm_() { + let mut h = harness(blank()); + sequence(&mut h, "HBM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBM must journal home.borders"); +} + +#[test] +fn keytip_hbn_() { + let mut h = harness(blank()); + sequence(&mut h, "HBN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBN must journal home.borders"); +} + +#[test] +fn keytip_hbo_() { + let mut h = harness(blank()); + sequence(&mut h, "HBO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBO must journal home.borders"); +} + +#[test] +fn keytip_hbp_() { + let mut h = harness(blank()); + sequence(&mut h, "HBP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBP must journal home.borders"); +} + +#[test] +fn keytip_hbr_() { + let mut h = harness(blank()); + sequence(&mut h, "HBR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBR must journal home.borders"); +} + +#[test] +fn keytip_hbs_() { + let mut h = harness(blank()); + sequence(&mut h, "HBS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBS must journal home.borders"); +} + +#[test] +fn keytip_hbt_() { + let mut h = harness(blank()); + sequence(&mut h, "HBT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBT must journal home.borders"); +} + +#[test] +fn keytip_hbu_() { + let mut h = harness(blank()); + sequence(&mut h, "HBU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBU must journal home.borders"); +} + +#[test] +fn keytip_hbw_() { + let mut h = harness(blank()); + sequence(&mut h, "HBW", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.drawBorder"), "HBW must journal home.drawBorder"); +} + +#[test] +fn keytip_hby_() { + let mut h = harness(blank()); + sequence(&mut h, "HBY", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.borders"), "HBY must journal home.borders"); +} + +#[test] +fn keytip_hc_() { + let mut h = harness(blank()); + sequence(&mut h, "HC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.copy"), "HC must journal edit.copy"); +} + +#[test] +fn keytip_hdc_() { + let mut h = harness(blank()); + sequence(&mut h, "HDC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.deleteColumns"), "HDC must journal home.deleteColumns"); +} + +#[test] +fn keytip_hdd_() { + let mut h = harness(blank()); + sequence(&mut h, "HDD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.deleteCells"), "HDD must journal home.deleteCells"); +} + +#[test] +fn keytip_hdr_() { + let mut h = harness(blank()); + sequence(&mut h, "HDR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.deleteRows"), "HDR must journal home.deleteRows"); +} + +#[test] +fn keytip_hds_() { + let mut h = harness(blank()); + sequence(&mut h, "HDS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HDS must invoke home.deleteSheet and surface its missing-params error"); +} + +#[test] +fn keytip_hea_() { + let mut h = harness(blank()); + sequence(&mut h, "HEA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearAll"), "HEA must journal edit.clearAll"); +} + +#[test] +fn keytip_hec_() { + let mut h = harness(blank()); + sequence(&mut h, "HEC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearContents"), "HEC must journal edit.clearContents"); +} + +#[test] +fn keytip_hef_() { + let mut h = harness(blank()); + sequence(&mut h, "HEF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearFormats"), "HEF must journal edit.clearFormats"); +} + +#[test] +fn keytip_hel_() { + let mut h = harness(blank()); + sequence(&mut h, "HEL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearHyperlinks"), "HEL must journal edit.clearHyperlinks"); +} + +#[test] +fn keytip_hem_() { + let mut h = harness(blank()); + sequence(&mut h, "HEM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.clearComments"), "HEM must journal edit.clearComments"); +} + +#[test] +fn keytip_hfc_() { + let mut h = harness(blank()); + sequence(&mut h, "HFC", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFC must invoke home.fontColor and surface its missing-params error"); +} + +#[test] +fn keytip_hfdc_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HFDC must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hfdf_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDF must invoke edit.find and surface its missing-params error"); +} + +#[test] +fn keytip_hfdg_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDG", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDG must invoke edit.goTo and surface its missing-params error"); +} + +#[test] +fn keytip_hfdm_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDM must invoke edit.find and surface its missing-params error"); +} + +#[test] +fn keytip_hfdo_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDO", false); + assert!(h.state().dialog.is_none(), "HFDO must not open a dialog"); + assert!(h.state().message.is_none(), "HFDO must not open a message box"); + assert!(h.state().toast.is_none(), "HFDO must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "HFDO must not journal"); +} + +#[test] +fn keytip_hfdp_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDP", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HFDP must run arrange.selectionPane cleanly"); +} + +#[test] +fn keytip_hfdr_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDR", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDR must invoke edit.replace and surface its missing-params error"); +} + +#[test] +fn keytip_hfds_() { + let mut h = harness(blank()); + sequence(&mut h, "HFDS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.goToSpecial"), "HFDS must journal edit.goToSpecial"); +} + +#[test] +fn keytip_hff_() { + let mut h = harness(blank()); + sequence(&mut h, "HFF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFF must invoke home.fontName and surface its missing-params error"); +} + +#[test] +fn keytip_hfg_() { + let mut h = harness(blank()); + sequence(&mut h, "HFG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.increaseFontSize"), "HFG must journal home.increaseFontSize"); +} + +#[test] +fn keytip_hfi_() { + let mut h = harness(blank()); + sequence(&mut h, "HFI", false); + assert_eq!(h.state().keytips.prefix(), Some("HFI"), "HFI must stay armed waiting for more input"); +} + +#[test] +fn keytip_hfia_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIA", false); + assert!(h.state().dialog.is_none(), "HFIA must not open a dialog"); + assert!(h.state().message.is_none(), "HFIA must not open a message box"); + assert!(h.state().toast.is_none(), "HFIA must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "HFIA must not journal"); +} + +#[test] +fn keytip_hfid_() { + let mut h = harness(blank()); + sequence(&mut h, "HFID", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillDown"), "HFID must journal edit.fillDown"); +} + +#[test] +fn keytip_hfif_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFIF must invoke edit.flashFill and surface its missing-params error"); +} + +#[test] +fn keytip_hfij_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIJ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.justify"), "HFIJ must journal home.justify"); +} + +#[test] +fn keytip_hfil_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillLeft"), "HFIL must journal edit.fillLeft"); +} + +#[test] +fn keytip_hfir_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillRight"), "HFIR must journal edit.fillRight"); +} + +#[test] +fn keytip_hfis_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillSeries"), "HFIS must journal edit.fillSeries"); +} + +#[test] +fn keytip_hfiu_() { + let mut h = harness(blank()); + sequence(&mut h, "HFIU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillUp"), "HFIU must journal edit.fillUp"); +} + +#[test] +fn keytip_hfk_() { + let mut h = harness(blank()); + sequence(&mut h, "HFK", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.decreaseFontSize"), "HFK must journal home.decreaseFontSize"); +} + +#[test] +fn keytip_hfm_() { + let mut h = harness(blank()); + sequence(&mut h, "HFM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFM must invoke home.numberFormat and surface its missing-params error"); +} + +#[test] +fn keytip_hfp_() { + let mut h = harness(blank()); + sequence(&mut h, "HFP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.formatPainter"), "HFP must journal edit.formatPainter"); +} + +#[test] +fn keytip_hfq_() { + let mut h = harness(blank()); + sequence(&mut h, "HFQ", false); + assert_eq!(h.state().keytips.prefix(), Some("HFQ"), "HFQ must stay armed waiting for more input"); +} + +#[test] +fn keytip_hfql_() { + let mut h = harness(blank()); + sequence(&mut h, "HFQL", false); + assert!(h.state().dialog.is_none(), "HFQL must not open a dialog"); + assert!(h.state().message.is_none(), "HFQL must not open a message box"); + assert!(h.state().toast.is_none(), "HFQL must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "HFQL must not journal"); +} + +#[test] +fn keytip_hfqo_() { + let mut h = harness(blank()); + sequence(&mut h, "HFQO", false); + assert!(h.state().dialog.is_none(), "HFQO must not open a dialog"); + assert!(h.state().message.is_none(), "HFQO must not open a message box"); + assert!(h.state().toast.is_none(), "HFQO must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "HFQO must not journal"); +} + +#[test] +fn keytip_hfs_() { + let mut h = harness(blank()); + sequence(&mut h, "HFS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFS must invoke home.fontSize and surface its missing-params error"); +} + +#[test] +fn keytip_hh_() { + let mut h = harness(blank()); + sequence(&mut h, "HH", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HH must invoke home.fillColor and surface its missing-params error"); +} + +#[test] +fn keytip_hic_() { + let mut h = harness(blank()); + sequence(&mut h, "HIC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertColumns"), "HIC must journal home.insertColumns"); +} + +#[test] +fn keytip_hie_() { + let mut h = harness(blank()); + sequence(&mut h, "HIE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertCells"), "HIE must journal home.insertCells"); +} + +#[test] +fn keytip_hii_() { + let mut h = harness(blank()); + sequence(&mut h, "HII", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertCells"), "HII must journal home.insertCells"); +} + +#[test] +fn keytip_hir_() { + let mut h = harness(blank()); + sequence(&mut h, "HIR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertRows"), "HIR must journal home.insertRows"); +} + +#[test] +fn keytip_his_() { + let mut h = harness(blank()); + sequence(&mut h, "HIS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertSheet"), "HIS must journal home.insertSheet"); +} + +#[test] +fn keytip_hj_() { + let mut h = harness(blank()); + sequence(&mut h, "HJ", false); + assert_eq!(h.state().keytips.prefix(), Some("HJ"), "HJ must stay armed waiting for more input"); +} + +#[test] +fn keytip_hjn_() { + let mut h = harness(blank()); + sequence(&mut h, "HJN", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HJN must invoke home.newCellStyle and surface its missing-params error"); +} + +#[test] +fn keytip_hk_() { + let mut h = harness(blank()); + sequence(&mut h, "HK", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.comma"), "HK must journal home.comma"); +} + +#[test] +fn keytip_hl_() { + let mut h = harness(blank()); + sequence(&mut h, "HL", false); + assert_eq!(h.state().keytips.prefix(), Some("HL"), "HL must stay armed waiting for more input"); +} + +#[test] +fn keytip_hlc_() { + let mut h = harness(blank()); + sequence(&mut h, "HLC", false); + assert_eq!(h.state().keytips.prefix(), Some("HLC"), "HLC must stay armed waiting for more input"); +} + +#[test] +fn keytip_hlce_() { + let mut h = harness(blank()); + sequence(&mut h, "HLCE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.clearRules"), "HLCE must journal home.clearRules"); +} + +#[test] +fn keytip_hlcs_() { + let mut h = harness(blank()); + sequence(&mut h, "HLCS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.clearRules"), "HLCS must journal home.clearRules"); +} + +#[test] +fn keytip_hlct_() { + let mut h = harness(blank()); + sequence(&mut h, "HLCT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.clearRules"), "HLCT must journal home.clearRules"); +} + +#[test] +fn keytip_hld_() { + let mut h = harness(blank()); + sequence(&mut h, "HLD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HLD must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hlh_() { + let mut h = harness(blank()); + sequence(&mut h, "HLH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HLH must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hln_() { + let mut h = harness(blank()); + sequence(&mut h, "HLN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HLN must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hlr_() { + let mut h = harness(blank()); + sequence(&mut h, "HLR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.manageRules"), "HLR must journal home.manageRules"); +} + +#[test] +fn keytip_hlt_() { + let mut h = harness(blank()); + sequence(&mut h, "HLT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.conditionalFormat"), "HLT must journal home.conditionalFormat"); +} + +#[test] +fn keytip_hm_() { + let mut h = harness(blank()); + sequence(&mut h, "HM", false); + assert_eq!(h.state().keytips.prefix(), Some("HM"), "HM must stay armed waiting for more input"); +} + +#[test] +fn keytip_hma_() { + let mut h = harness(blank()); + sequence(&mut h, "HMA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.mergeAcross"), "HMA must journal home.mergeAcross"); +} + +#[test] +fn keytip_hmc_() { + let mut h = harness(blank()); + sequence(&mut h, "HMC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.mergeCenter"), "HMC must journal home.mergeCenter"); +} + +#[test] +fn keytip_hmm_() { + let mut h = harness(blank()); + sequence(&mut h, "HMM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.mergeCells"), "HMM must journal home.mergeCells"); +} + +#[test] +fn keytip_hmu_() { + let mut h = harness(blank()); + sequence(&mut h, "HMU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.unmergeCells"), "HMU must journal home.unmergeCells"); +} + +#[test] +fn keytip_hn_() { + let mut h = harness(blank()); + sequence(&mut h, "HN", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HN must invoke home.numberFormat and surface its missing-params error"); +} + +#[test] +fn keytip_hoa_() { + let mut h = harness(blank()); + sequence(&mut h, "HOA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.autofitRowHeight"), "HOA must journal home.autofitRowHeight"); +} + +#[test] +fn keytip_hod_() { + let mut h = harness(blank()); + sequence(&mut h, "HOD", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOD must invoke home.columnWidth and surface its missing-params error"); +} + +#[test] +fn keytip_hoe_() { + let mut h = harness(blank()); + sequence(&mut h, "HOE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.formatCells"), "HOE must journal home.formatCells"); +} + +#[test] +fn keytip_hoh_() { + let mut h = harness(blank()); + sequence(&mut h, "HOH", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOH must invoke home.rowHeight and surface its missing-params error"); +} + +#[test] +fn keytip_hol_() { + let mut h = harness(blank()); + sequence(&mut h, "HOL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.lockCell"), "HOL must journal home.lockCell"); +} + +#[test] +fn keytip_hom_() { + let mut h = harness(blank()); + sequence(&mut h, "HOM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOM must invoke sheet.move and surface its missing-params error"); +} + +#[test] +fn keytip_hop_() { + let mut h = harness(blank()); + sequence(&mut h, "HOP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectSheet"), "HOP must journal review.protectSheet"); +} + +#[test] +fn keytip_hor_() { + let mut h = harness(blank()); + sequence(&mut h, "HOR", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOR must invoke sheet.rename and surface its missing-params error"); +} + +#[test] +fn keytip_hot_() { + let mut h = harness(blank()); + sequence(&mut h, "HOT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "sheet.tabColor"), "HOT must journal sheet.tabColor"); +} + +#[test] +fn keytip_houc_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.hideColumns"), "HOUC must journal home.hideColumns"); +} + +#[test] +fn keytip_houh_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUH", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOUH must invoke sheet.unhide and surface its missing-params error"); +} + +#[test] +fn keytip_houl_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.unhideColumns"), "HOUL must journal home.unhideColumns"); +} + +#[test] +fn keytip_houo_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.unhideRows"), "HOUO must journal home.unhideRows"); +} + +#[test] +fn keytip_hour_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.hideRows"), "HOUR must journal home.hideRows"); +} + +#[test] +fn keytip_hous_() { + let mut h = harness(blank()); + sequence(&mut h, "HOUS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOUS must invoke sheet.hide and surface its missing-params error"); +} + +#[test] +fn keytip_how_() { + let mut h = harness(blank()); + sequence(&mut h, "HOW", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOW must invoke home.columnWidth and surface its missing-params error"); +} + +#[test] +fn keytip_hp_() { + let mut h = harness(blank()); + sequence(&mut h, "HP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.percent"), "HP must journal home.percent"); +} + +#[test] +fn keytip_hsc_() { + let mut h = harness(blank()); + sequence(&mut h, "HSC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.clearFilter"), "HSC must journal data.clearFilter"); +} + +#[test] +fn keytip_hsf_() { + let mut h = harness(blank()); + sequence(&mut h, "HSF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.filter"), "HSF must journal data.filter"); +} + +#[test] +fn keytip_hso_() { + let mut h = harness(blank()); + sequence(&mut h, "HSO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sortDescending"), "HSO must journal data.sortDescending"); +} + +#[test] +fn keytip_hss_() { + let mut h = harness(blank()); + sequence(&mut h, "HSS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sortAscending"), "HSS must journal data.sortAscending"); +} + +#[test] +fn keytip_hsu_() { + let mut h = harness(blank()); + sequence(&mut h, "HSU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sort"), "HSU must journal data.sort"); +} + +#[test] +fn keytip_ht_() { + let mut h = harness(blank()); + sequence(&mut h, "HT", false); + assert_eq!(h.state().keytips.prefix(), Some("HT"), "HT must stay armed waiting for more input"); +} + +#[test] +fn keytip_htn_() { + let mut h = harness(blank()); + sequence(&mut h, "HTN", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HTN must invoke table.style and surface its missing-params error"); +} + +#[test] +fn keytip_htp_() { + let mut h = harness(blank()); + sequence(&mut h, "HTP", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HTP must invoke table.style and surface its missing-params error"); +} + +#[test] +fn keytip_hu_() { + let mut h = harness(blank()); + sequence(&mut h, "HU", false); + assert_eq!(h.state().keytips.prefix(), Some("HU"), "HU must stay armed waiting for more input"); +} + +#[test] +fn keytip_hua_() { + let mut h = harness(blank()); + sequence(&mut h, "HUA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "HUA must journal formulas.autoSum"); +} + +#[test] +fn keytip_huc_() { + let mut h = harness(blank()); + sequence(&mut h, "HUC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "HUC must journal formulas.autoSum"); +} + +#[test] +fn keytip_hui_() { + let mut h = harness(blank()); + sequence(&mut h, "HUI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "HUI must journal formulas.autoSum"); +} + +#[test] +fn keytip_hum_() { + let mut h = harness(blank()); + sequence(&mut h, "HUM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "HUM must journal formulas.autoSum"); +} + +#[test] +fn keytip_hv_() { + let mut h = harness(blank()); + sequence(&mut h, "HV", false); + assert_eq!(h.state().keytips.prefix(), Some("HV"), "HV must stay armed waiting for more input"); +} + +#[test] +fn keytip_hva_() { + let mut h = harness(blank()); + sequence(&mut h, "HVA", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVA must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvb_() { + let mut h = harness(blank()); + sequence(&mut h, "HVB", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVB must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hve_() { + let mut h = harness(blank()); + sequence(&mut h, "HVE", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVE must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvf_() { + let mut h = harness(blank()); + sequence(&mut h, "HVF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVF must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvi_() { + let mut h = harness(blank()); + sequence(&mut h, "HVI", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVI must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvk_() { + let mut h = harness(blank()); + sequence(&mut h, "HVK", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVK must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvm_() { + let mut h = harness(blank()); + sequence(&mut h, "HVM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVM must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvn_() { + let mut h = harness(blank()); + sequence(&mut h, "HVN", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVN must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvo_() { + let mut h = harness(blank()); + sequence(&mut h, "HVO", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVO must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvp_() { + let mut h = harness(blank()); + sequence(&mut h, "HVP", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVP must invoke edit.paste and surface its missing-params error"); +} + +#[test] +fn keytip_hvr_() { + let mut h = harness(blank()); + sequence(&mut h, "HVR", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVR must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvt_() { + let mut h = harness(blank()); + sequence(&mut h, "HVT", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVT must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvu_() { + let mut h = harness(blank()); + sequence(&mut h, "HVU", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVU must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvv_() { + let mut h = harness(blank()); + sequence(&mut h, "HVV", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVV must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hvw_() { + let mut h = harness(blank()); + sequence(&mut h, "HVW", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVW must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_hw_() { + let mut h = harness(blank()); + sequence(&mut h, "HW", false); + assert_eq!(h.state().keytips.prefix(), Some("HW"), "HW must stay armed waiting for more input"); +} + +#[test] +fn keytip_hws_() { + let mut h = harness(blank()); + sequence(&mut h, "HWS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.shrinkToFit"), "HWS must journal home.shrinkToFit"); +} + +#[test] +fn keytip_hww_() { + let mut h = harness(blank()); + sequence(&mut h, "HWW", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.wrapText"), "HWW must journal home.wrapText"); +} + +#[test] +fn keytip_hx_() { + let mut h = harness(blank()); + sequence(&mut h, "HX", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.cut"), "HX must journal edit.cut"); +} + +#[test] +fn keytip_nc1_() { + let mut h = harness(blank()); + sequence(&mut h, "NC1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NC1 must journal insert.chart"); +} + +#[test] +fn keytip_nc2_() { + let mut h = harness(blank()); + sequence(&mut h, "NC2", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newComment"), "NC2 must journal review.newComment"); +} + +#[test] +fn keytip_ncm_() { + let mut h = harness(blank()); + sequence(&mut h, "NCM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newComment"), "NCM must journal review.newComment"); +} + +#[test] +fn keytip_ncb_() { + let mut h = harness(blank()); + sequence(&mut h, "NCB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.checkbox"), "NCB must journal insert.checkbox"); +} + +#[test] +fn keytip_nd_() { + let mut h = harness(blank()); + sequence(&mut h, "ND", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "ND must journal insert.chart"); +} + +#[test] +fn keytip_ne_() { + let mut h = harness(blank()); + sequence(&mut h, "NE", false); + assert!(h.state().dialog.is_none(), "NE must not open a dialog"); + assert!(h.state().message.is_none(), "NE must not open a message box"); + assert!(h.state().toast.is_none(), "NE must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NE must not journal"); +} + +#[test] +fn keytip_nf_() { + let mut h = harness(blank()); + sequence(&mut h, "NF", false); + assert_eq!(h.state().keytips.prefix(), Some("NF"), "NF must stay armed waiting for more input"); +} + +#[test] +fn keytip_nfp_() { + let mut h = harness(blank()); + sequence(&mut h, "NFP", false); + assert_eq!(h.state().keytips.prefix(), Some("NFP"), "NFP must stay armed waiting for more input"); +} + +#[test] +fn keytip_nfpf_() { + let mut h = harness(blank()); + sequence(&mut h, "NFPF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NFPF must journal insert.picture"); +} + +#[test] +fn keytip_nh1_() { + let mut h = harness(blank()); + sequence(&mut h, "NH1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.headerFooter"), "NH1 must journal pageLayout.headerFooter"); +} + +#[test] +fn keytip_nhf_() { + let mut h = harness(blank()); + sequence(&mut h, "NHF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.headerFooter"), "NHF must journal pageLayout.headerFooter"); +} + +#[test] +fn keytip_nhi_() { + let mut h = harness(blank()); + sequence(&mut h, "NHI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NHI must journal insert.chart"); +} + +#[test] +fn keytip_ni1_() { + let mut h = harness(blank()); + sequence(&mut h, "NI1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NI1 must journal insert.chart"); +} + +#[test] +fn keytip_ni2_() { + let mut h = harness(blank()); + sequence(&mut h, "NI2", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.link"), "NI2 must journal insert.link"); +} + +#[test] +fn keytip_nj_() { + let mut h = harness(blank()); + sequence(&mut h, "NJ", false); + assert!(h.state().dialog.is_none(), "NJ must not open a dialog"); + assert!(h.state().message.is_none(), "NJ must not open a message box"); + assert!(h.state().toast.is_none(), "NJ must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NJ must not journal"); +} + +#[test] +fn keytip_nm1t_() { + let mut h = harness(blank()); + sequence(&mut h, "NM1T", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NM1T must journal insert.picture"); +} + +#[test] +fn keytip_nm2_() { + let mut h = harness(blank()); + sequence(&mut h, "NM2", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NM2 must journal insert.chart"); +} + +#[test] +fn keytip_nn1_() { + let mut h = harness(blank()); + sequence(&mut h, "NN1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NN1 must journal insert.chart"); +} + +#[test] +fn keytip_nns_() { + let mut h = harness(blank()); + sequence(&mut h, "NNS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.icons"), "NNS must journal insert.icons"); +} + +#[test] +fn keytip_np_() { + let mut h = harness(blank()); + sequence(&mut h, "NP", false); + assert_eq!(h.state().keytips.prefix(), Some("NP"), "NP must stay armed waiting for more input"); +} + +#[test] +fn keytip_npb_() { + let mut h = harness(blank()); + sequence(&mut h, "NPB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NPB must journal insert.picture"); +} + +#[test] +fn keytip_npf_() { + let mut h = harness(blank()); + sequence(&mut h, "NPF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NPF must journal insert.picture"); +} + +#[test] +fn keytip_npi_() { + let mut h = harness(blank()); + sequence(&mut h, "NPI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NPI must journal insert.picture"); +} + +#[test] +fn keytip_npo_() { + let mut h = harness(blank()); + sequence(&mut h, "NPO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "NPO must journal insert.picture"); +} + +#[test] +fn keytip_nq_() { + let mut h = harness(blank()); + sequence(&mut h, "NQ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NQ must journal insert.chart"); +} + +#[test] +fn keytip_nr_() { + let mut h = harness(blank()); + sequence(&mut h, "NR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.recommendedCharts"), "NR must journal insert.recommendedCharts"); +} + +#[test] +fn keytip_nsa_() { + let mut h = harness(blank()); + sequence(&mut h, "NSA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NSA must journal insert.chart"); +} + +#[test] +fn keytip_nsc_() { + let mut h = harness(blank()); + sequence(&mut h, "NSC", false); + assert!(h.state().dialog.is_none(), "NSC must not open a dialog"); + assert!(h.state().message.is_none(), "NSC must not open a message box"); + assert!(h.state().toast.is_none(), "NSC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NSC must not journal"); +} + +#[test] +fn keytip_nsd_() { + let mut h = harness(blank()); + sequence(&mut h, "NSD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NSD must journal insert.chart"); +} + +#[test] +fn keytip_nsf_() { + let mut h = harness(blank()); + sequence(&mut h, "NSF", false); + assert!(h.state().dialog.is_none(), "NSF must not open a dialog"); + assert!(h.state().message.is_none(), "NSF must not open a message box"); + assert!(h.state().toast.is_none(), "NSF must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NSF must not journal"); +} + +#[test] +fn keytip_nsh_() { + let mut h = harness(blank()); + sequence(&mut h, "NSH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.shape"), "NSH must journal insert.shape"); +} + +#[test] +fn keytip_nsp_() { + let mut h = harness(blank()); + sequence(&mut h, "NSP", false); + assert!( + h.state().message.is_some() || h.state().toast.is_some(), + "NSP must invoke insert.recommendedPivotTables and surface its missing-params error" + ); +} + +#[test] +fn keytip_nst_() { + let mut h = harness(blank()); + sequence(&mut h, "NST", false); + assert!(h.state().dialog.is_none(), "NST must not open a dialog"); + assert!(h.state().message.is_none(), "NST must not open a message box"); + assert!(h.state().toast.is_none(), "NST must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NST must not journal"); +} + +#[test] +fn keytip_nsz_() { + let mut h = harness(blank()); + sequence(&mut h, "NSZ", false); + assert!(h.state().dialog.is_none(), "NSZ must not open a dialog"); + assert!(h.state().message.is_none(), "NSZ must not open a message box"); + assert!(h.state().toast.is_none(), "NSZ must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NSZ must not journal"); +} + +#[test] +fn keytip_nt_() { + let mut h = harness(blank()); + sequence(&mut h, "NT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.table"), "NT must journal insert.table"); +} + +#[test] +fn keytip_nu_() { + let mut h = harness(blank()); + sequence(&mut h, "NU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.symbol"), "NU must journal insert.symbol"); +} + +#[test] +fn keytip_nzs_() { + let mut h = harness(blank()); + sequence(&mut h, "NZS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.symbol"), "NZS must journal insert.symbol"); +} + +#[test] +fn keytip_nv_() { + let mut h = harness(blank()); + sequence(&mut h, "NV", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NV must invoke insert.pivotTable and surface its missing-params error"); +} + +#[test] +fn keytip_nw_() { + let mut h = harness(blank()); + sequence(&mut h, "NW", false); + assert!(h.state().dialog.is_none(), "NW must not open a dialog"); + assert!(h.state().message.is_none(), "NW must not open a message box"); + assert!(h.state().toast.is_none(), "NW must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "NW must not journal"); +} + +#[test] +fn keytip_nx_() { + let mut h = harness(blank()); + sequence(&mut h, "NX", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.textBox"), "NX must journal insert.textBox"); +} + +#[test] +fn keytip_nzk_() { + let mut h = harness(blank()); + sequence(&mut h, "NZK", false); + assert_eq!(h.state().keytips.prefix(), Some("NZK"), "NZK must stay armed waiting for more input"); +} + +#[test] +fn keytip_nzksl_() { + let mut h = harness(blank()); + sequence(&mut h, "NZKSL", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSL must invoke insert.sparkline and surface its missing-params error"); +} + +#[test] +fn keytip_nzkso_() { + let mut h = harness(blank()); + sequence(&mut h, "NZKSO", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSO must invoke insert.sparkline and surface its missing-params error"); +} + +#[test] +fn keytip_nzksw_() { + let mut h = harness(blank()); + sequence(&mut h, "NZKSW", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSW must invoke insert.sparkline and surface its missing-params error"); +} + +#[test] +fn keytip_nztx_() { + let mut h = harness(blank()); + sequence(&mut h, "NZTX", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.textBox"), "NZTX must journal insert.textBox"); +} + +#[test] +fn keytip_nzt_() { + let mut h = harness(blank()); + sequence(&mut h, "NZT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.textBox"), "NZT must journal insert.textBox"); +} + +#[test] +fn keytip_pae_() { + let mut h = harness(blank()); + sequence(&mut h, "PAE", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "PAE must invoke arrange.sendBackward and surface its missing-params error"); +} + +#[test] +fn keytip_paf_() { + let mut h = harness(blank()); + sequence(&mut h, "PAF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "PAF must invoke arrange.bringForward and surface its missing-params error"); +} + +#[test] +fn keytip_pb_() { + let mut h = harness(blank()); + sequence(&mut h, "PB", false); + assert_eq!(h.state().keytips.prefix(), Some("PB"), "PB must stay armed waiting for more input"); +} + +#[test] +fn keytip_pbi_() { + let mut h = harness(blank()); + sequence(&mut h, "PBI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.breaks"), "PBI must journal pageLayout.breaks"); +} + +#[test] +fn keytip_pbr_() { + let mut h = harness(blank()); + sequence(&mut h, "PBR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.breaks"), "PBR must journal pageLayout.breaks"); +} + +#[test] +fn keytip_pg_() { + let mut h = harness(blank()); + sequence(&mut h, "PG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.background"), "PG must journal pageLayout.background"); +} + +#[test] +fn keytip_pi_() { + let mut h = harness(blank()); + sequence(&mut h, "PI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.printTitles"), "PI must journal pageLayout.printTitles"); +} + +#[test] +fn keytip_pm_() { + let mut h = harness(blank()); + sequence(&mut h, "PM", false); + assert_eq!(h.state().keytips.prefix(), Some("PM"), "PM must stay armed waiting for more input"); +} + +#[test] +fn keytip_pma_() { + let mut h = harness(blank()); + sequence(&mut h, "PMA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.margins"), "PMA must journal pageLayout.margins"); +} + +#[test] +fn keytip_po_() { + let mut h = harness(blank()); + sequence(&mut h, "PO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.orientation"), "PO must journal pageLayout.orientation"); +} + +#[test] +fn keytip_ppg_() { + let mut h = harness(blank()); + sequence(&mut h, "PPG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.printGridlines"), "PPG must journal pageLayout.printGridlines"); +} + +#[test] +fn keytip_pph_() { + let mut h = harness(blank()); + sequence(&mut h, "PPH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.printHeadings"), "PPH must journal pageLayout.printHeadings"); +} + +#[test] +fn keytip_pr_() { + let mut h = harness(blank()); + sequence(&mut h, "PR", false); + assert_eq!(h.state().keytips.prefix(), Some("PR"), "PR must stay armed waiting for more input"); +} + +#[test] +fn keytip_prs_() { + let mut h = harness(blank()); + sequence(&mut h, "PRS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.printArea"), "PRS must journal pageLayout.printArea"); +} + +#[test] +fn keytip_psp_() { + let mut h = harness(blank()); + sequence(&mut h, "PSP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.pageSetup"), "PSP must journal pageLayout.pageSetup"); +} + +#[test] +fn keytip_psz_() { + let mut h = harness(blank()); + sequence(&mut h, "PSZ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.size"), "PSZ must journal pageLayout.size"); +} + +#[test] +fn keytip_ptc_() { + let mut h = harness(blank()); + sequence(&mut h, "PTC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.themeColors"), "PTC must journal pageLayout.themeColors"); +} + +#[test] +fn keytip_pte_() { + let mut h = harness(blank()); + sequence(&mut h, "PTE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.themeEffects"), "PTE must journal pageLayout.themeEffects"); +} + +#[test] +fn keytip_ptf_() { + let mut h = harness(blank()); + sequence(&mut h, "PTF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.themeFonts"), "PTF must journal pageLayout.themeFonts"); +} + +#[test] +fn keytip_pth_() { + let mut h = harness(blank()); + sequence(&mut h, "PTH", false); + assert_eq!(h.state().keytips.prefix(), Some("PTH"), "PTH must stay armed waiting for more input"); +} + +#[test] +fn keytip_pthb_() { + let mut h = harness(blank()); + sequence(&mut h, "PTHB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.theme"), "PTHB must journal pageLayout.theme"); +} + +#[test] +fn keytip_pvg_() { + let mut h = harness(blank()); + sequence(&mut h, "PVG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines"), "PVG must journal view.gridlines"); +} + +#[test] +fn keytip_pvh_() { + let mut h = harness(blank()); + sequence(&mut h, "PVH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.headings"), "PVH must journal view.headings"); +} + +#[test] +fn keytip_ma_() { + let mut h = harness(blank()); + sequence(&mut h, "MA", false); + assert_eq!(h.state().keytips.prefix(), Some("MA"), "MA must stay armed waiting for more input"); +} + +#[test] +fn keytip_maa_() { + let mut h = harness(blank()); + sequence(&mut h, "MAA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.removeArrows"), "MAA must journal formulas.removeArrows"); +} + +#[test] +fn keytip_map_() { + let mut h = harness(blank()); + sequence(&mut h, "MAP", false); + assert!(h.state().dialog.is_none(), "MAP must not open a dialog"); + assert!(h.state().message.is_none(), "MAP must not open a message box"); + assert!(h.state().toast.is_none(), "MAP must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "MAP must not journal"); +} + +#[test] +fn keytip_mb_() { + let mut h = harness(blank()); + sequence(&mut h, "MB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculateNow"), "MB must journal formulas.calculateNow"); +} + +#[test] +fn keytip_mc_() { + let mut h = harness(blank()); + sequence(&mut h, "MC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.createFromSelection"), "MC must journal formulas.createFromSelection"); +} + +#[test] +fn keytip_md_() { + let mut h = harness(blank()); + sequence(&mut h, "MD", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MD must run formulas.traceDependents cleanly"); +} + +#[test] +fn keytip_me_() { + let mut h = harness(blank()); + sequence(&mut h, "ME", false); + assert_eq!(h.state().keytips.prefix(), Some("ME"), "ME must stay armed waiting for more input"); +} + +#[test] +fn keytip_mef_() { + let mut h = harness(blank()); + sequence(&mut h, "MEF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MEF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mf_() { + let mut h = harness(blank()); + sequence(&mut h, "MF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mg_() { + let mut h = harness(blank()); + sequence(&mut h, "MG", false); + assert_eq!(h.state().keytips.prefix(), Some("MG"), "MG must stay armed waiting for more input"); +} + +#[test] +fn keytip_mgf_() { + let mut h = harness(blank()); + sequence(&mut h, "MGF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MGF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mh_() { + let mut h = harness(blank()); + sequence(&mut h, "MH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.showFormulas"), "MH must journal formulas.showFormulas"); +} + +#[test] +fn keytip_mi_() { + let mut h = harness(blank()); + sequence(&mut h, "MI", false); + assert_eq!(h.state().keytips.prefix(), Some("MI"), "MI must stay armed waiting for more input"); +} + +#[test] +fn keytip_mi1_() { + let mut h = harness(blank()); + sequence(&mut h, "MI1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MI1 must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mif_() { + let mut h = harness(blank()); + sequence(&mut h, "MIF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MIF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mj_() { + let mut h = harness(blank()); + sequence(&mut h, "MJ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculateSheet"), "MJ must journal formulas.calculateSheet"); +} + +#[test] +fn keytip_mk_() { + let mut h = harness(blank()); + sequence(&mut h, "MK", false); + assert_eq!(h.state().keytips.prefix(), Some("MK"), "MK must stay armed waiting for more input"); +} + +#[test] +fn keytip_mkk_() { + let mut h = harness(blank()); + sequence(&mut h, "MKK", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MKK must run formulas.errorChecking cleanly"); +} + +#[test] +fn keytip_ml_() { + let mut h = harness(blank()); + sequence(&mut h, "ML", false); + assert_eq!(h.state().keytips.prefix(), Some("ML"), "ML must stay armed waiting for more input"); +} + +#[test] +fn keytip_mlf_() { + let mut h = harness(blank()); + sequence(&mut h, "MLF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MLF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mm_() { + let mut h = harness(blank()); + sequence(&mut h, "MM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.defineName"), "MM must journal formulas.defineName"); +} + +#[test] +fn keytip_mn_() { + let mut h = harness(blank()); + sequence(&mut h, "MN", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MN must run formulas.nameManager cleanly"); +} + +#[test] +fn keytip_mo_() { + let mut h = harness(blank()); + sequence(&mut h, "MO", false); + assert_eq!(h.state().keytips.prefix(), Some("MO"), "MO must stay armed waiting for more input"); +} + +#[test] +fn keytip_mof_() { + let mut h = harness(blank()); + sequence(&mut h, "MOF", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MOF must run formulas.lookupReference cleanly"); +} + +#[test] +fn keytip_mp_() { + let mut h = harness(blank()); + sequence(&mut h, "MP", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MP must run formulas.tracePrecedents cleanly"); +} + +#[test] +fn keytip_mq_() { + let mut h = harness(blank()); + sequence(&mut h, "MQ", false); + assert_eq!(h.state().keytips.prefix(), Some("MQ"), "MQ must stay armed waiting for more input"); +} + +#[test] +fn keytip_mqcf_() { + let mut h = harness(blank()); + sequence(&mut h, "MQCF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQCF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqef_() { + let mut h = harness(blank()); + sequence(&mut h, "MQEF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQEF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqif_() { + let mut h = harness(blank()); + sequence(&mut h, "MQIF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQIF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqof_() { + let mut h = harness(blank()); + sequence(&mut h, "MQOF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQOF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqsf_() { + let mut h = harness(blank()); + sequence(&mut h, "MQSF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQSF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mqwf_() { + let mut h = harness(blank()); + sequence(&mut h, "MQWF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQWF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mr_() { + let mut h = harness(blank()); + sequence(&mut h, "MR", false); + assert_eq!(h.state().keytips.prefix(), Some("MR"), "MR must stay armed waiting for more input"); +} + +#[test] +fn keytip_mrf_() { + let mut h = harness(blank()); + sequence(&mut h, "MRF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MRF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_ms_() { + let mut h = harness(blank()); + sequence(&mut h, "MS", false); + assert_eq!(h.state().keytips.prefix(), Some("MS"), "MS must stay armed waiting for more input"); +} + +#[test] +fn keytip_msp_() { + let mut h = harness(blank()); + sequence(&mut h, "MSP", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "MSP must invoke edit.pasteSpecial and surface its missing-params error"); +} + +#[test] +fn keytip_mt_() { + let mut h = harness(blank()); + sequence(&mut h, "MT", false); + assert_eq!(h.state().keytips.prefix(), Some("MT"), "MT must stay armed waiting for more input"); +} + +#[test] +fn keytip_mtf_() { + let mut h = harness(blank()); + sequence(&mut h, "MTF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MTF must journal formulas.insertFunction"); +} + +#[test] +fn keytip_mu_() { + let mut h = harness(blank()); + sequence(&mut h, "MU", false); + assert_eq!(h.state().keytips.prefix(), Some("MU"), "MU must stay armed waiting for more input"); +} + +#[test] +fn keytip_mua_() { + let mut h = harness(blank()); + sequence(&mut h, "MUA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUA must journal formulas.autoSum"); +} + +#[test] +fn keytip_muc_() { + let mut h = harness(blank()); + sequence(&mut h, "MUC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUC must journal formulas.autoSum"); +} + +#[test] +fn keytip_muf_() { + let mut h = harness(blank()); + sequence(&mut h, "MUF", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MUF must run formulas.moreFunctions cleanly"); +} + +#[test] +fn keytip_mui_() { + let mut h = harness(blank()); + sequence(&mut h, "MUI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUI must journal formulas.autoSum"); +} + +#[test] +fn keytip_mum_() { + let mut h = harness(blank()); + sequence(&mut h, "MUM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUM must journal formulas.autoSum"); +} + +#[test] +fn keytip_mus_() { + let mut h = harness(blank()); + sequence(&mut h, "MUS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.autoSum"), "MUS must journal formulas.autoSum"); +} + +#[test] +fn keytip_mv_() { + let mut h = harness(blank()); + sequence(&mut h, "MV", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "MV must invoke formulas.evaluateFormula and surface its missing-params error"); +} + +#[test] +fn keytip_mw_() { + let mut h = harness(blank()); + sequence(&mut h, "MW", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MW must run formulas.watchWindow cleanly"); +} + +#[test] +fn keytip_mx_() { + let mut h = harness(blank()); + sequence(&mut h, "MX", false); + assert_eq!(h.state().keytips.prefix(), Some("MX"), "MX must stay armed waiting for more input"); +} + +#[test] +fn keytip_mxa_() { + let mut h = harness(blank()); + sequence(&mut h, "MXA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculationOptions"), "MXA must journal formulas.calculationOptions"); +} + +#[test] +fn keytip_mxm_() { + let mut h = harness(blank()); + sequence(&mut h, "MXM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculationOptions"), "MXM must journal formulas.calculationOptions"); +} + +#[test] +fn keytip_mxp_() { + let mut h = harness(blank()); + sequence(&mut h, "MXP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.calculationOptions"), "MXP must journal formulas.calculationOptions"); +} + +#[test] +fn keytip_ab_() { + let mut h = harness(blank()); + sequence(&mut h, "AB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.subtotal"), "AB must journal data.subtotal"); +} + +#[test] +fn keytip_ac_() { + let mut h = harness(blank()); + sequence(&mut h, "AC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.clearFilter"), "AC must journal data.clearFilter"); +} + +#[test] +fn keytip_ad_() { + let mut h = harness(blank()); + sequence(&mut h, "AD", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AD must invoke data.consolidate and surface its missing-params error"); +} + +#[test] +fn keytip_ae_() { + let mut h = harness(blank()); + sequence(&mut h, "AE", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.textToColumns"), "AE must journal data.textToColumns"); +} + +#[test] +fn keytip_af1_() { + let mut h = harness(blank()); + sequence(&mut h, "AF1", false); + assert_eq!(h.state().keytips.prefix(), Some("AF1"), "AF1 must stay armed waiting for more input"); +} + +#[test] +fn keytip_af1c_() { + let mut h = harness(blank()); + sequence(&mut h, "AF1C", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "AF1C must journal insert.picture"); +} + +#[test] +fn keytip_af1p_() { + let mut h = harness(blank()); + sequence(&mut h, "AF1P", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.picture"), "AF1P must journal insert.picture"); +} + +#[test] +fn keytip_afa_() { + let mut h = harness(blank()); + sequence(&mut h, "AFA", false); + assert!(h.state().dialog.is_none(), "AFA must not open a dialog"); + assert!(h.state().message.is_none(), "AFA must not open a message box"); + assert!(h.state().toast.is_none(), "AFA must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFA must not journal"); +} + +#[test] +fn keytip_afc_() { + let mut h = harness(blank()); + sequence(&mut h, "AFC", false); + assert!(h.state().dialog.is_none(), "AFC must not open a dialog"); + assert!(h.state().message.is_none(), "AFC must not open a message box"); + assert!(h.state().toast.is_none(), "AFC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFC must not journal"); +} + +#[test] +fn keytip_afe_() { + let mut h = harness(blank()); + sequence(&mut h, "AFE", false); + assert!(h.state().dialog.is_none(), "AFE must not open a dialog"); + assert!(h.state().message.is_none(), "AFE must not open a message box"); + assert!(h.state().toast.is_none(), "AFE must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFE must not journal"); +} + +#[test] +fn keytip_aff_() { + let mut h = harness(blank()); + sequence(&mut h, "AFF", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AFF must invoke edit.flashFill and surface its missing-params error"); +} + +#[test] +fn keytip_afo_() { + let mut h = harness(blank()); + sequence(&mut h, "AFO", false); + assert!(h.state().dialog.is_none(), "AFO must not open a dialog"); + assert!(h.state().message.is_none(), "AFO must not open a message box"); + assert!(h.state().toast.is_none(), "AFO must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFO must not journal"); +} + +#[test] +fn keytip_aft_() { + let mut h = harness(blank()); + sequence(&mut h, "AFT", false); + assert!(h.state().dialog.is_none(), "AFT must not open a dialog"); + assert!(h.state().message.is_none(), "AFT must not open a message box"); + assert!(h.state().toast.is_none(), "AFT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFT must not journal"); +} + +#[test] +fn keytip_afw_() { + let mut h = harness(blank()); + sequence(&mut h, "AFW", false); + assert!(h.state().dialog.is_none(), "AFW must not open a dialog"); + assert!(h.state().message.is_none(), "AFW must not open a message box"); + assert!(h.state().toast.is_none(), "AFW must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AFW must not journal"); +} + +#[test] +fn keytip_ag_() { + let mut h = harness(blank()); + sequence(&mut h, "AG", false); + assert_eq!(h.state().keytips.prefix(), Some("AG"), "AG must stay armed waiting for more input"); +} + +#[test] +fn keytip_aga_() { + let mut h = harness(blank()); + sequence(&mut h, "AGA", false); + assert!(h.state().dialog.is_none(), "AGA must not open a dialog"); + assert!(h.state().message.is_none(), "AGA must not open a message box"); + assert!(h.state().toast.is_none(), "AGA must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AGA must not journal"); +} + +#[test] +fn keytip_agg_() { + let mut h = harness(blank()); + sequence(&mut h, "AGG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.group"), "AGG must journal data.group"); +} + +#[test] +fn keytip_ah_() { + let mut h = harness(blank()); + sequence(&mut h, "AH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.hideDetail"), "AH must journal data.hideDetail"); +} + +#[test] +fn keytip_aj_() { + let mut h = harness(blank()); + sequence(&mut h, "AJ", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.showDetail"), "AJ must journal data.showDetail"); +} + +#[test] +fn keytip_ak_() { + let mut h = harness(blank()); + sequence(&mut h, "AK", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "AK must run data.queriesConnections cleanly"); +} + +#[test] +fn keytip_am_() { + let mut h = harness(blank()); + sequence(&mut h, "AM", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.removeDuplicates"), "AM must journal data.removeDuplicates"); +} + +#[test] +fn keytip_ao_() { + let mut h = harness(blank()); + sequence(&mut h, "AO", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "AO must run data.queriesConnections cleanly"); +} + +#[test] +fn keytip_ap1_() { + let mut h = harness(blank()); + sequence(&mut h, "AP1", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.properties"), "AP1 must journal file.properties"); +} + +#[test] +fn keytip_apn_() { + let mut h = harness(blank()); + sequence(&mut h, "APN", false); + assert_eq!(h.state().keytips.prefix(), Some("APN"), "APN must stay armed waiting for more input"); +} + +#[test] +fn keytip_apnd_() { + let mut h = harness(blank()); + sequence(&mut h, "APND", false); + assert!(h.state().dialog.is_none(), "APND must not open a dialog"); + assert!(h.state().message.is_none(), "APND must not open a message box"); + assert!(h.state().toast.is_none(), "APND must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APND must not journal"); +} + +#[test] +fn keytip_apne_() { + let mut h = harness(blank()); + sequence(&mut h, "APNE", false); + assert!(h.state().dialog.is_none(), "APNE must not open a dialog"); + assert!(h.state().message.is_none(), "APNE must not open a message box"); + assert!(h.state().toast.is_none(), "APNE must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNE must not journal"); +} + +#[test] +fn keytip_apng_() { + let mut h = harness(blank()); + sequence(&mut h, "APNG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.getData"), "APNG must journal data.getData"); +} + +#[test] +fn keytip_apnh_() { + let mut h = harness(blank()); + sequence(&mut h, "APNH", false); + assert!(h.state().dialog.is_none(), "APNH must not open a dialog"); + assert!(h.state().message.is_none(), "APNH must not open a message box"); + assert!(h.state().toast.is_none(), "APNH must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNH must not journal"); +} + +#[test] +fn keytip_apnl_() { + let mut h = harness(blank()); + sequence(&mut h, "APNL", false); + assert!(h.state().dialog.is_none(), "APNL must not open a dialog"); + assert!(h.state().message.is_none(), "APNL must not open a message box"); + assert!(h.state().toast.is_none(), "APNL must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNL must not journal"); +} + +#[test] +fn keytip_apns_() { + let mut h = harness(blank()); + sequence(&mut h, "APNS", false); + assert!(h.state().dialog.is_none(), "APNS must not open a dialog"); + assert!(h.state().message.is_none(), "APNS must not open a message box"); + assert!(h.state().toast.is_none(), "APNS must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNS must not journal"); +} + +#[test] +fn keytip_apnt_() { + let mut h = harness(blank()); + sequence(&mut h, "APNT", false); + assert!(h.state().dialog.is_none(), "APNT must not open a dialog"); + assert!(h.state().message.is_none(), "APNT must not open a message box"); + assert!(h.state().toast.is_none(), "APNT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APNT must not journal"); +} + +#[test] +fn keytip_apr_() { + let mut h = harness(blank()); + sequence(&mut h, "APR", false); + assert!(h.state().dialog.is_none(), "APR must not open a dialog"); + assert!(h.state().message.is_none(), "APR must not open a message box"); + assert!(h.state().toast.is_none(), "APR must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APR must not journal"); +} + +#[test] +fn keytip_aps_() { + let mut h = harness(blank()); + sequence(&mut h, "APS", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "APS must run data.queriesConnections cleanly"); +} + +#[test] +fn keytip_apt_() { + let mut h = harness(blank()); + sequence(&mut h, "APT", false); + assert!(h.state().dialog.is_none(), "APT must not open a dialog"); + assert!(h.state().message.is_none(), "APT must not open a message box"); + assert!(h.state().toast.is_none(), "APT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "APT must not journal"); +} + +#[test] +fn keytip_aq_() { + let mut h = harness(blank()); + sequence(&mut h, "AQ", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AQ must invoke data.advancedFilter and surface its missing-params error"); +} + +#[test] +fn keytip_ar_() { + let mut h = harness(blank()); + sequence(&mut h, "AR", false); + assert_eq!(h.state().keytips.prefix(), Some("AR"), "AR must stay armed waiting for more input"); +} + +#[test] +fn keytip_ara_() { + let mut h = harness(blank()); + sequence(&mut h, "ARA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.refreshAll"), "ARA must journal data.refreshAll"); +} + +#[test] +fn keytip_arc_() { + let mut h = harness(blank()); + sequence(&mut h, "ARC", false); + assert!(h.state().dialog.is_none(), "ARC must not open a dialog"); + assert!(h.state().message.is_none(), "ARC must not open a message box"); + assert!(h.state().toast.is_none(), "ARC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "ARC must not journal"); +} + +#[test] +fn keytip_aro_() { + let mut h = harness(blank()); + sequence(&mut h, "ARO", false); + assert!(h.state().dialog.is_none(), "ARO must not open a dialog"); + assert!(h.state().message.is_none(), "ARO must not open a message box"); + assert!(h.state().toast.is_none(), "ARO must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "ARO must not journal"); +} + +#[test] +fn keytip_arr_() { + let mut h = harness(blank()); + sequence(&mut h, "ARR", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "ARR must invoke pivot.refresh and surface its missing-params error"); +} + +#[test] +fn keytip_ars_() { + let mut h = harness(blank()); + sequence(&mut h, "ARS", false); + assert!(h.state().dialog.is_none(), "ARS must not open a dialog"); + assert!(h.state().message.is_none(), "ARS must not open a message box"); + assert!(h.state().toast.is_none(), "ARS must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "ARS must not journal"); +} + +#[test] +fn keytip_asa_() { + let mut h = harness(blank()); + sequence(&mut h, "ASA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sortAscending"), "ASA must journal data.sortAscending"); +} + +#[test] +fn keytip_asd_() { + let mut h = harness(blank()); + sequence(&mut h, "ASD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sortDescending"), "ASD must journal data.sortDescending"); +} + +#[test] +fn keytip_ass_() { + let mut h = harness(blank()); + sequence(&mut h, "ASS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sort"), "ASS must journal data.sort"); +} + +#[test] +fn keytip_at_() { + let mut h = harness(blank()); + sequence(&mut h, "AT", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.filter"), "AT must journal data.filter"); +} + +#[test] +fn keytip_au_() { + let mut h = harness(blank()); + sequence(&mut h, "AU", false); + assert_eq!(h.state().keytips.prefix(), Some("AU"), "AU must stay armed waiting for more input"); +} + +#[test] +fn keytip_auc_() { + let mut h = harness(blank()); + sequence(&mut h, "AUC", false); + assert!(h.state().dialog.is_none(), "AUC must not open a dialog"); + assert!(h.state().message.is_none(), "AUC must not open a message box"); + assert!(h.state().toast.is_none(), "AUC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AUC must not journal"); +} + +#[test] +fn keytip_auu_() { + let mut h = harness(blank()); + sequence(&mut h, "AUU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.ungroup"), "AUU must journal data.ungroup"); +} + +#[test] +fn keytip_av_() { + let mut h = harness(blank()); + sequence(&mut h, "AV", false); + assert_eq!(h.state().keytips.prefix(), Some("AV"), "AV must stay armed waiting for more input"); +} + +#[test] +fn keytip_avi_() { + let mut h = harness(blank()); + sequence(&mut h, "AVI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.circleInvalid"), "AVI must journal data.circleInvalid"); +} + +#[test] +fn keytip_avr_() { + let mut h = harness(blank()); + sequence(&mut h, "AVR", false); + assert!(h.state().dialog.is_none(), "AVR must not open a dialog"); + assert!(h.state().message.is_none(), "AVR must not open a message box"); + assert!(h.state().toast.is_none(), "AVR must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AVR must not journal"); +} + +#[test] +fn keytip_avv_() { + let mut h = harness(blank()); + sequence(&mut h, "AVV", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.validation"), "AVV must journal data.validation"); +} + +#[test] +fn keytip_aw_() { + let mut h = harness(blank()); + sequence(&mut h, "AW", false); + assert_eq!(h.state().keytips.prefix(), Some("AW"), "AW must stay armed waiting for more input"); +} + +#[test] +fn keytip_awg_() { + let mut h = harness(blank()); + sequence(&mut h, "AWG", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AWG must invoke data.goalSeek and surface its missing-params error"); +} + +#[test] +fn keytip_aws_() { + let mut h = harness(blank()); + sequence(&mut h, "AWS", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "AWS must run data.scenarioManager cleanly"); +} + +#[test] +fn keytip_awt_() { + let mut h = harness(blank()); + sequence(&mut h, "AWT", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AWT must invoke data.dataTable and surface its missing-params error"); +} + +#[test] +fn keytip_ax_() { + let mut h = harness(blank()); + sequence(&mut h, "AX", false); + assert!(h.state().dialog.is_none(), "AX must not open a dialog"); + assert!(h.state().message.is_none(), "AX must not open a message box"); + assert!(h.state().toast.is_none(), "AX must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "AX must not journal"); +} + +#[test] +fn keytip_ay_() { + let mut h = harness(blank()); + sequence(&mut h, "AY", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.reapply"), "AY must journal data.reapply"); +} + +#[test] +fn keytip_ra1_() { + let mut h = harness(blank()); + sequence(&mut h, "RA1", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "RA1 must run review.checkAccessibility cleanly"); +} + +#[test] +fn keytip_ra2_() { + let mut h = harness(blank()); + sequence(&mut h, "RA2", false); + assert!(h.state().dialog.is_none(), "RA2 must not open a dialog"); + assert!(h.state().message.is_none(), "RA2 must not open a message box"); + assert!(h.state().toast.is_none(), "RA2 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RA2 must not journal"); +} + +#[test] +fn keytip_rc_() { + let mut h = harness(blank()); + sequence(&mut h, "RC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newComment"), "RC must journal review.newComment"); +} + +#[test] +fn keytip_rd_() { + let mut h = harness(blank()); + sequence(&mut h, "RD", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.deleteComment"), "RD must journal review.deleteComment"); +} + +#[test] +fn keytip_rg_() { + let mut h = harness(blank()); + sequence(&mut h, "RG", false); + assert!(h.state().dialog.is_none(), "RG must not open a dialog"); + assert!(h.state().message.is_none(), "RG must not open a message box"); + assert!(h.state().toast.is_none(), "RG must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RG must not journal"); +} + +#[test] +fn keytip_rl_() { + let mut h = harness(blank()); + sequence(&mut h, "RL", false); + assert!(h.state().dialog.is_none(), "RL must not open a dialog"); + assert!(h.state().message.is_none(), "RL must not open a message box"); + assert!(h.state().toast.is_none(), "RL must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RL must not journal"); +} + +#[test] +fn keytip_rn_() { + let mut h = harness(blank()); + sequence(&mut h, "RN", false); + assert!(h.state().dialog.is_none(), "RN must not open a dialog"); + assert!(h.state().message.is_none(), "RN must not open a message box"); + assert!(h.state().toast.is_none(), "RN must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RN must not journal"); +} + +#[test] +fn keytip_ro_() { + let mut h = harness(blank()); + sequence(&mut h, "RO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectWorkbook"), "RO must journal review.protectWorkbook"); +} + +#[test] +fn keytip_rps_() { + let mut h = harness(blank()); + sequence(&mut h, "RPS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectSheet"), "RPS must journal review.protectSheet"); +} + +#[test] +fn keytip_rpw_() { + let mut h = harness(blank()); + sequence(&mut h, "RPW", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectWorkbook"), "RPW must journal review.protectWorkbook"); +} + +#[test] +fn keytip_rrs_() { + let mut h = harness(blank()); + sequence(&mut h, "RRS", false); + assert!(h.state().dialog.is_none(), "RRS must not open a dialog"); + assert!(h.state().message.is_none(), "RRS must not open a message box"); + assert!(h.state().toast.is_none(), "RRS must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RRS must not journal"); +} + +#[test] +fn keytip_rs_() { + let mut h = harness(blank()); + sequence(&mut h, "RS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "RS must invoke review.spelling and surface its missing-params error"); +} + +#[test] +fn keytip_rtn_() { + let mut h = harness(blank()); + sequence(&mut h, "RTN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newNote"), "RTN must journal review.newNote"); +} + +#[test] +fn keytip_rv_() { + let mut h = harness(blank()); + sequence(&mut h, "RV", false); + assert!(h.state().dialog.is_none(), "RV must not open a dialog"); + assert!(h.state().message.is_none(), "RV must not open a message box"); + assert!(h.state().toast.is_none(), "RV must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RV must not journal"); +} + +#[test] +fn keytip_rw_() { + let mut h = harness(blank()); + sequence(&mut h, "RW", false); + assert!(h.state().dialog.is_none(), "RW must not open a dialog"); + assert!(h.state().message.is_none(), "RW must not open a message box"); + assert!(h.state().toast.is_none(), "RW must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "RW must not journal"); +} + +#[test] +fn keytip_wa_() { + let mut h = harness(blank()); + sequence(&mut h, "WA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.arrangeAll"), "WA must journal view.arrangeAll"); +} + +#[test] +fn keytip_wb_() { + let mut h = harness(blank()); + sequence(&mut h, "WB", false); + assert!(h.state().dialog.is_none(), "WB must not open a dialog"); + assert!(h.state().message.is_none(), "WB must not open a message box"); + assert!(h.state().toast.is_none(), "WB must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WB must not journal"); +} + +#[test] +fn keytip_wc_() { + let mut h = harness(blank()); + sequence(&mut h, "WC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.customViews"), "WC must journal view.customViews"); +} + +#[test] +fn keytip_wf_() { + let mut h = harness(blank()); + sequence(&mut h, "WF", false); + assert_eq!(h.state().keytips.prefix(), Some("WF"), "WF must stay armed waiting for more input"); +} + +#[test] +fn keytip_wfc_() { + let mut h = harness(blank()); + sequence(&mut h, "WFC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.freezeFirstColumn"), "WFC must journal view.freezeFirstColumn"); +} + +#[test] +fn keytip_wff_() { + let mut h = harness(blank()); + sequence(&mut h, "WFF", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.freezePanes"), "WFF must journal view.freezePanes"); +} + +#[test] +fn keytip_wfr_() { + let mut h = harness(blank()); + sequence(&mut h, "WFR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.freezeTopRow"), "WFR must journal view.freezeTopRow"); +} + +#[test] +fn keytip_wg_() { + let mut h = harness(blank()); + sequence(&mut h, "WG", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.zoomToSelection"), "WG must journal view.zoomToSelection"); +} + +#[test] +fn keytip_wh_() { + let mut h = harness(blank()); + sequence(&mut h, "WH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.hideWindow"), "WH must journal view.hideWindow"); +} + +#[test] +fn keytip_wi_() { + let mut h = harness(blank()); + sequence(&mut h, "WI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.pageBreakPreview"), "WI must journal view.pageBreakPreview"); +} + +#[test] +fn keytip_wj_() { + let mut h = harness(blank()); + sequence(&mut h, "WJ", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "WJ must invoke view.zoom and surface its missing-params error"); +} + +#[test] +fn keytip_wl_() { + let mut h = harness(blank()); + sequence(&mut h, "WL", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.normal"), "WL must journal view.normal"); +} + +#[test] +fn keytip_wm_() { + let mut h = harness(blank()); + sequence(&mut h, "WM", false); + assert_eq!(h.state().keytips.prefix(), Some("WM"), "WM must stay armed waiting for more input"); +} + +#[test] +fn keytip_wmr_() { + let mut h = harness(blank()); + sequence(&mut h, "WMR", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WMR must run view.macros cleanly"); +} + +#[test] +fn keytip_wmv_() { + let mut h = harness(blank()); + sequence(&mut h, "WMV", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WMV must run view.macros cleanly"); +} + +#[test] +fn keytip_wn_() { + let mut h = harness(blank()); + sequence(&mut h, "WN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.newWindow"), "WN must journal view.newWindow"); +} + +#[test] +fn keytip_wp_() { + let mut h = harness(blank()); + sequence(&mut h, "WP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.pageLayout"), "WP must journal view.pageLayout"); +} + +#[test] +fn keytip_wq_() { + let mut h = harness(blank()); + sequence(&mut h, "WQ", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "WQ must invoke view.zoom and surface its missing-params error"); +} + +#[test] +fn keytip_wr_() { + let mut h = harness(blank()); + sequence(&mut h, "WR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.ruler"), "WR must journal view.ruler"); +} + +#[test] +fn keytip_ws_() { + let mut h = harness(blank()); + sequence(&mut h, "WS", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.split"), "WS must journal view.split"); +} + +#[test] +fn keytip_wt_() { + let mut h = harness(blank()); + sequence(&mut h, "WT", false); + assert!(h.state().dialog.is_none(), "WT must not open a dialog"); + assert!(h.state().message.is_none(), "WT must not open a message box"); + assert!(h.state().toast.is_none(), "WT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WT must not journal"); +} + +#[test] +fn keytip_wu_() { + let mut h = harness(blank()); + sequence(&mut h, "WU", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.unhideWindow"), "WU must journal view.unhideWindow"); +} + +#[test] +fn keytip_wvc_() { + let mut h = harness(blank()); + sequence(&mut h, "WVC", false); + assert!(h.state().dialog.is_none(), "WVC must not open a dialog"); + assert!(h.state().message.is_none(), "WVC must not open a message box"); + assert!(h.state().toast.is_none(), "WVC must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVC must not journal"); +} + +#[test] +fn keytip_wvf_() { + let mut h = harness(blank()); + sequence(&mut h, "WVF", false); + assert!(h.state().dialog.is_none(), "WVF must not open a dialog"); + assert!(h.state().message.is_none(), "WVF must not open a message box"); + assert!(h.state().toast.is_none(), "WVF must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVF must not journal"); +} + +#[test] +fn keytip_wvh_() { + let mut h = harness(blank()); + sequence(&mut h, "WVH", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.headings"), "WVH must journal view.headings"); +} + +#[test] +fn keytip_wvl_() { + let mut h = harness(blank()); + sequence(&mut h, "WVL", false); + assert!(h.state().dialog.is_none(), "WVL must not open a dialog"); + assert!(h.state().message.is_none(), "WVL must not open a message box"); + assert!(h.state().toast.is_none(), "WVL must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVL must not journal"); +} + +#[test] +fn keytip_wvn_() { + let mut h = harness(blank()); + sequence(&mut h, "WVN", false); + assert!(h.state().dialog.is_none(), "WVN must not open a dialog"); + assert!(h.state().message.is_none(), "WVN must not open a message box"); + assert!(h.state().toast.is_none(), "WVN must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVN must not journal"); +} + +#[test] +fn keytip_wvr_() { + let mut h = harness(blank()); + sequence(&mut h, "WVR", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.ruler"), "WVR must journal view.ruler"); +} + +#[test] +fn keytip_wvv_() { + let mut h = harness(blank()); + sequence(&mut h, "WVV", false); + assert!(h.state().dialog.is_none(), "WVV must not open a dialog"); + assert!(h.state().message.is_none(), "WVV must not open a message box"); + assert!(h.state().toast.is_none(), "WVV must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "WVV must not journal"); +} + +#[test] +fn keytip_ww_() { + let mut h = harness(blank()); + sequence(&mut h, "WW", false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WW must run view.switchWindows cleanly"); +} + +#[test] +fn keytip_jam_() { + let mut h = harness(blank()); + sequence(&mut h, "JAM", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JAM must invoke chart.formatSelection and surface its missing-params error"); +} + +#[test] +fn keytip_jca_() { + let mut h = harness(blank()); + sequence(&mut h, "JCA", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JCA must invoke chart.addElement and surface its missing-params error"); +} + +#[test] +fn keytip_jcc_() { + let mut h = harness(blank()); + sequence(&mut h, "JCC", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JCC must invoke chart.changeType and surface its missing-params error"); +} + +#[test] +fn keytip_jts_() { + let mut h = harness(blank()); + sequence(&mut h, "JTS", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JTS must invoke table.style and surface its missing-params error"); +} + +#[test] +fn keytip_jtv_() { + let mut h = harness(blank()); + sequence(&mut h, "JTV", false); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "JTV must invoke table.summarizePivot and surface its missing-params error"); +} + +#[test] +fn keytip_fa_() { + let mut h = harness(blank()); + sequence(&mut h, "FA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.saveAs"), "FA must journal file.saveAs"); +} + +#[test] +fn keytip_fc_() { + let mut h = harness(blank()); + sequence(&mut h, "FC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.close"), "FC must journal file.close"); +} + +#[test] +fn keytip_fd_() { + let mut h = harness(blank()); + sequence(&mut h, "FD", false); + assert!(h.state().dialog.is_none(), "FD must not open a dialog"); + assert!(h.state().message.is_none(), "FD must not open a message box"); + assert!(h.state().toast.is_none(), "FD must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "FD must not journal"); +} + +#[test] +fn keytip_fea_() { + let mut h = harness(blank()); + sequence(&mut h, "FEA", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.exportPdf"), "FEA must journal file.exportPdf"); +} + +#[test] +fn keytip_fec_() { + let mut h = harness(blank()); + sequence(&mut h, "FEC", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.saveAs"), "FEC must journal file.saveAs"); +} + +#[test] +fn keytip_fh_() { + let mut h = harness(blank()); + sequence(&mut h, "FH", false); + assert!(h.state().dialog.is_none(), "FH must not open a dialog"); + assert!(h.state().message.is_none(), "FH must not open a message box"); + assert!(h.state().toast.is_none(), "FH must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "FH must not journal"); +} + +#[test] +fn keytip_fi_() { + let mut h = harness(blank()); + sequence(&mut h, "FI", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.properties"), "FI must journal file.properties"); +} + +#[test] +fn keytip_fn_() { + let mut h = harness(blank()); + sequence(&mut h, "FN", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.new"), "FN must journal file.new"); +} + +#[test] +fn keytip_fo_() { + let mut h = harness(blank()); + sequence(&mut h, "FO", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.open"), "FO must journal file.open"); +} + +#[test] +fn keytip_fp_() { + let mut h = harness(blank()); + sequence(&mut h, "FP", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.print"), "FP must journal file.print"); +} + +#[test] +fn keytip_ft_() { + let mut h = harness(blank()); + sequence(&mut h, "FT", false); + assert!(h.state().dialog.is_none(), "FT must not open a dialog"); + assert!(h.state().message.is_none(), "FT must not open a message box"); + assert!(h.state().toast.is_none(), "FT must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "FT must not journal"); +} + +#[test] +fn keytip_q_() { + let mut h = harness(blank()); + sequence(&mut h, "Q", false); + assert!(h.state().dialog.is_none(), "Q must not open a dialog"); + assert!(h.state().message.is_none(), "Q must not open a message box"); + assert!(h.state().toast.is_none(), "Q must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "Q must not journal"); +} + +#[test] +fn keytip_6_() { + let mut h = harness(blank()); + sequence(&mut h, "6", false); + assert!(h.state().dialog.is_none(), "6 must not open a dialog"); + assert!(h.state().message.is_none(), "6 must not open a message box"); + assert!(h.state().toast.is_none(), "6 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "6 must not journal"); +} + +#[test] +fn keytip_7_() { + let mut h = harness(blank()); + sequence(&mut h, "7", false); + assert!(h.state().dialog.is_none(), "7 must not open a dialog"); + assert!(h.state().message.is_none(), "7 must not open a message box"); + assert!(h.state().toast.is_none(), "7 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "7 must not journal"); +} + +#[test] +fn keytip_8_() { + let mut h = harness(blank()); + sequence(&mut h, "8", false); + assert!(h.state().dialog.is_none(), "8 must not open a dialog"); + assert!(h.state().message.is_none(), "8 must not open a message box"); + assert!(h.state().toast.is_none(), "8 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "8 must not journal"); +} + +#[test] +fn keytip_9_() { + let mut h = harness(blank()); + sequence(&mut h, "9", false); + assert!(h.state().dialog.is_none(), "9 must not open a dialog"); + assert!(h.state().message.is_none(), "9 must not open a message box"); + assert!(h.state().toast.is_none(), "9 must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "9 must not journal"); +} + +#[test] +fn generated_test_count_matches_the_table() { + const EXPECTED: usize = 412; + assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED); +} + +#[test] +fn sample_sequences_work_with_text_before_key() { + // Some IME paths deliver Event::Text ahead of the key press; the matcher + // must pair them either way. One representative of each action kind; + // the dialog opener runs last so it does not eat later input. + // Each step resets first: Escape twice leaves keytip mode so the next + // F10 arms cleanly (a lingering prefix would make F10 disarm instead). + let mut h = harness(blank()); + sequence(&mut h, "H", true); + assert_eq!(h.state().ui.ribbon_tab, "Home"); + reset(&mut h); + sequence(&mut h, "WVG", true); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines")); + reset(&mut h); + sequence(&mut h, "HV", true); + assert_eq!(h.state().keytips.prefix(), Some("HV")); + reset(&mut h); + sequence(&mut h, "NE", true); + assert!(h.state().dialog.is_none() && h.state().message.is_none() && h.state().toast.is_none()); + reset(&mut h); + sequence(&mut h, "HVS", true); + assert!(h.state().dialog.is_some(), "HVS must open the paste-special dialog"); +} + +fn reset(h: &mut Harness) { + key(h, Key::Escape); // clear any prefix, back to the armed root + key(h, Key::Escape); // leave keytip mode entirely +} diff --git a/xtask/src/keytips.rs b/xtask/src/keytips.rs index baa498c..4e3c1dd 100644 --- a/xtask/src/keytips.rs +++ b/xtask/src/keytips.rs @@ -1,13 +1,22 @@ -//! Regenerate the keytip sequence table from `keytips.json`. +//! Regenerate the keytip sequence table and its per-sequence test suite from `keytips.json`. //! -//! `cargo xtask keytips` regenerate crates/ui-egui/src/keytips/generated.rs -//! `cargo xtask keytips --check` fail if the generated file differs from the JSON source +//! `cargo xtask keytips` regenerate the table + `tests/keytips_generated.rs` +//! `cargo xtask keytips --check` fail if either generated file differs from the JSON source +//! +//! Requires the default `engine` feature: the generator probes the live command +//! catalog so each generated test asserts exactly what the command does today +//! (journaled, ran clean without a journal step, or surfaced a missing-params error). use std::fs; use std::path::Path; const TABS: &[&str] = &["Home", "Insert", "Draw", "Page Layout", "Formulas", "Data", "Review", "View", "Automate"]; +/// Commands whose empty-params behaviour depends on the host machine (e.g. the +/// system dictionary). These are never probed; their tests assert "invoked +/// something observable" so the checked-in file stays byte-identical everywhere. +const ENV_SENSITIVE: &[&str] = &["review.spelling"]; + /// Format generated source through `rustfmt` so the checked-in file matches /// the emitter byte-for-byte. fn rustfmt(src: &str) -> Result { @@ -29,92 +38,175 @@ fn rustfmt(src: &str) -> Result { Ok(String::from_utf8_lossy(&out.stdout).into_owned()) } -pub fn run(root: &Path, rest: &[&str]) -> Result<(), String> { - let json_path = root.join("crates/ui-egui/src/keytips/keytips.json"); - let out_path = root.join("crates/ui-egui/src/keytips/keytip_table/generated.rs"); - let raw = generate(&fs::read_to_string(&json_path).map_err(|e| format!("read {}: {e}", json_path.display()))?)?; - // The table is checked into the tree formatted, so emit through rustfmt: - // otherwise `--check` always fails against the fmt'd file. - let generated = rustfmt(&raw)?; - if rest.contains(&"--check") { - let on_disk = fs::read_to_string(&out_path).map_err(|e| format!("read {}: {e}", out_path.display()))?; - if on_disk != generated { - return Err(format!("{} is out of date — run `cargo xtask keytips` and commit the result", out_path.display())); - } - println!("keytips: {} up to date", out_path.display()); - return Ok(()); - } - fs::write(&out_path, &generated).map_err(|e| format!("write {}: {e}", out_path.display()))?; - println!("keytips: wrote {}", out_path.display()); - Ok(()) +#[derive(Clone)] +struct Entry { + seq: String, + kind: String, + target: String, + pending: bool, + excel: String, + note: String, } -pub fn generate(source: &str) -> Result { +/// 0 = ran and journaled, 1 = ran without a journal step, 2 = errored (params +/// missing, disabled, or environment-dependent). +type Probe = std::collections::HashMap; + +#[cfg(feature = "engine")] +fn probe_live_commands(ids: &[String]) -> Probe { + use gridcraft_engine::Session; + let mut map = Probe::new(); + for id in ids { + if ENV_SENSITIVE.contains(&id.as_str()) { + continue; + } + let mut s = Session::new(); + s.new_workbook(); + let status = match s.execute(id, serde_json::json!({})) { + Ok(_) => { + if s.journal.iter().any(|(jid, _)| jid == id) { + 0 + } else { + 1 + } + } + Err(_) => 2, + }; + map.insert(id.clone(), status); + } + map +} + +pub fn run(root: &Path, rest: &[&str]) -> Result<(), String> { + #[cfg(not(feature = "engine"))] + { + let _ = (root, rest); + return Err("cargo xtask keytips needs the default `engine` feature to probe the command catalog".into()); + } + #[cfg(feature = "engine")] + { + let json_path = root.join("crates/ui-egui/src/keytips/keytips.json"); + let table_path = root.join("crates/ui-egui/src/keytips/keytip_table/generated.rs"); + let tests_path = root.join("crates/ui-egui/tests/keytips_generated.rs"); + let source = fs::read_to_string(&json_path).map_err(|e| format!("read {}: {e}", json_path.display()))?; + let (entries, probe) = parse_and_probe(&source)?; + let generated = + [(table_path.as_path(), rustfmt(&render_table(&entries))?), (tests_path.as_path(), rustfmt(&render_tests(&entries, &probe))?)]; + if rest.contains(&"--check") { + for (path, expected) in &generated { + let on_disk = fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?; + if on_disk != *expected { + return Err(format!("{} is out of date — run `cargo xtask keytips` and commit the result", path.display())); + } + println!("keytips: {} up to date", path.display()); + } + return Ok(()); + } + for (path, content) in &generated { + fs::write(path, content).map_err(|e| format!("write {}: {e}", path.display()))?; + println!("keytips: wrote {}", path.display()); + } + Ok(()) + } +} + +#[cfg(feature = "engine")] +fn parse_and_probe(source: &str) -> Result<(Vec, Probe), String> { let doc: serde_json::Value = serde_json::from_str(source).map_err(|e| format!("invalid JSON: {e}"))?; let seqs = doc.get("sequences").and_then(|v| v.as_array()).ok_or("missing `sequences` array")?; - let mut rows: Vec<(String, String, String)> = Vec::new(); + let mut entries: Vec = Vec::new(); let mut seen = std::collections::HashSet::new(); - for (i, entry) in seqs.iter().enumerate() { + for (i, e) in seqs.iter().enumerate() { let at = format!("entry {}", i + 1); - let seq = entry.get("seq").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `seq`"))?; + let seq = e.get("seq").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `seq`"))?.to_string(); if seq.is_empty() || seq.len() > 6 || !seq.bytes().all(|b| b.is_ascii_alphanumeric()) { return Err(format!("{at}: seq {seq:?} must be 1-6 ASCII letters/digits (Excel's longest are 5, e.g. sparkline paths)")); } - if !seen.insert(seq.to_string()) { + if !seen.insert(seq.clone()) { return Err(format!("{at}: duplicate seq {seq:?}")); } - let kind = entry.get("kind").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `kind`"))?; - let target = entry.get("target").and_then(|v| v.as_str()).unwrap_or(""); - let pending = entry.get("pending").and_then(|v| v.as_bool()).unwrap_or(false); - let note = entry.get("note").and_then(|v| v.as_str()).unwrap_or(""); - let excel = entry.get("excel").and_then(|v| v.as_str()).unwrap_or(""); + let kind = e.get("kind").and_then(|v| v.as_str()).ok_or_else(|| format!("{at}: missing `kind`"))?.to_string(); + let target = e.get("target").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let pending = e.get("pending").and_then(|v| v.as_bool()).unwrap_or(false); if pending && kind != "command" { return Err(format!("{at}: `pending` is only valid with kind `command`")); } - let value = match kind { + match kind.as_str() { "tab" => { - if target.is_empty() || !TABS.contains(&target) { + if target.is_empty() || !TABS.contains(&target.as_str()) { return Err(format!("{at}: tab target {target:?} is not one of {TABS:?}")); } - format!("Entry::Tab({target:?})") } - "command" => { + "command" | "dialog" => { if target.is_empty() { - return Err(format!("{at}: command needs a `target` command id")); + return Err(format!("{at}: {kind} needs a `target`")); } - if pending { format!("Entry::Finish(Action::Pending({target:?}))") } else { format!("Entry::Finish(Action::Command({target:?}))") } } - "dialog" => { - if target.is_empty() { - return Err(format!("{at}: dialog needs a `target` dialog name")); - } - format!("Entry::Finish(Action::Dialog({target:?}))") - } - "continue" => "Entry::Continue".to_string(), + "continue" => {} other => return Err(format!("{at}: unknown kind {other:?}")), - }; - let mut comment = String::new(); - if pending { - comment.push_str(" pending"); } - if !excel.is_empty() { - if !comment.is_empty() { - comment.push(';'); + // A terminal Finish entry on a strict prefix makes the longer sequence + // unreachable (the prefix fires and clears first), e.g. HC (Copy) kills HCP. + if kind == "command" || kind == "dialog" { + for cut in 1..seq.len() { + let prefix = &seq[..cut]; + if entries.iter().any(|e| e.seq == prefix && (e.kind == "command" || e.kind == "dialog")) { + return Err(format!( + "{at}: seq {seq:?} is unreachable — {prefix:?} already fires first; the deeper item needs a menu (item-letter), not a trie sequence" + )); + } } - comment.push_str(&format!(" Excel: {excel}")); } - if !note.is_empty() { - if !comment.is_empty() { - comment.push(';'); - } - comment.push_str(&format!(" {note}")); - } - rows.push((seq.to_string(), value, comment)); + entries.push(Entry { + seq, + kind, + target, + pending, + excel: e.get("excel").and_then(|v| v.as_str()).unwrap_or("").to_string(), + note: e.get("note").and_then(|v| v.as_str()).unwrap_or("").to_string(), + }); } // JSON order is authoritative and curated: the first entry for a tab is its canonical // badge (legacy aliases come later), and groups stay together. Do not sort. + let live_ids: Vec = entries + .iter() + .filter(|e| e.kind == "command" && !e.pending) + .map(|e| e.target.clone()) + .collect::>() + .into_iter() + .collect(); + // Pending targets that already exist fire live at runtime (T6); their tests + // must assert the live behaviour, not the silent one. Nudge the maintainer. + let landed: std::collections::BTreeSet = entries + .iter() + .filter(|e| e.pending && gridcraft_engine::find_command(&e.target).is_some()) + .map(|e| { + eprintln!("keytips: note — pending target {:?} now exists in the catalog; consider dropping `pending`", e.target); + e.target.clone() + }) + .collect(); + let mut probe = probe_live_commands(&live_ids); + for id in landed { + let mut s = gridcraft_engine::Session::new(); + s.new_workbook(); + let status = match s.execute(&id, serde_json::json!({})) { + Ok(_) => { + if s.journal.iter().any(|(jid, _)| jid == &id) { + 0 + } else { + 1 + } + } + Err(_) => 2, + }; + probe.insert(id, status); + } + Ok((entries, probe)) +} + +fn render_table(entries: &[Entry]) -> String { let mut out = String::from( "//! GENERATED from keytips.json by `cargo xtask keytips`. Edit the JSON, not this file.\n\n\ use super::{Action, Entry};\n\n\ @@ -123,13 +215,191 @@ pub fn generate(source: &str) -> Result { /// listed here. See keytip_table.rs for the helpers that read this table.\n\ pub const SEQUENCES: &[(&str, Entry)] = &[\n", ); - for (seq, value, comment) in &rows { - out.push_str(&format!(" ({seq:?}, {value}),")); + for e in entries { + let value = match e.kind.as_str() { + "tab" => format!("Entry::Tab({:?})", e.target), + "command" if e.pending => format!("Entry::Finish(Action::Pending({:?}))", e.target), + "command" => format!("Entry::Finish(Action::Command({:?}))", e.target), + "dialog" => format!("Entry::Finish(Action::Dialog({:?}))", e.target), + "continue" => "Entry::Continue".to_string(), + // All kinds are validated during parse; anything else cannot occur. + _ => String::new(), + }; + let mut comment = String::new(); + if e.pending { + comment.push_str(" pending"); + } + if !e.excel.is_empty() { + if !comment.is_empty() { + comment.push(';'); + } + comment.push_str(&format!(" Excel: {}", e.excel)); + } + if !e.note.is_empty() { + if !comment.is_empty() { + comment.push(';'); + } + comment.push_str(&format!(" {}", e.note)); + } + out.push_str(&format!(" ({:?}, {value}),", e.seq)); if !comment.is_empty() { out.push_str(&format!(" //{comment}")); } out.push('\n'); } out.push_str("];\n"); - Ok(out) + out } + +fn test_name(seq: &str) -> String { + format!("keytip_{}_", seq.to_ascii_lowercase()) +} + +fn render_tests(entries: &[Entry], probe: &Probe) -> String { + let mut out = String::from(HEADER); + for e in entries { + let body = match e.kind.as_str() { + "tab" => format!( + "sequence(&mut h, {seq:?}, false);\n assert_eq!(h.state().ui.ribbon_tab, {tab:?}, \"{seq} must switch to the {tab} tab\");", + seq = e.seq, + tab = e.target, + ), + "command" if e.pending && !probe.contains_key(&e.target) => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().dialog.is_none(), \"{seq} must not open a dialog\");\n assert!(h.state().message.is_none(), \"{seq} must not open a message box\");\n assert!(h.state().toast.is_none(), \"{seq} must not raise a toast\");\n assert!(h.state().session.journal.is_empty(), \"{seq} must not journal\");", + seq = e.seq, + ), + "command" => match probe.get(&e.target).copied().unwrap_or(2) { + 0 => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().session.journal.iter().any(|(id, _)| id == {id:?}), \"{seq} must journal {id}\");", + seq = e.seq, + id = e.target, + ), + 1 => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().message.is_none() && h.state().toast.is_none(), \"{seq} must run {id} cleanly\");", + seq = e.seq, + id = e.target, + ), + _ => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().message.is_some() || h.state().toast.is_some(), \"{seq} must invoke {id} and surface its missing-params error\");", + seq = e.seq, + id = e.target, + ), + }, + "dialog" => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().dialog.is_some(), \"{seq} must open the {dialog} dialog\");", + seq = e.seq, + dialog = e.target, + ), + "continue" => format!( + "sequence(&mut h, {seq:?}, false);\n assert_eq!(h.state().keytips.prefix(), Some({seq:?}), \"{seq} must stay armed waiting for more input\");", + seq = e.seq, + ), + // All kinds are validated during parse; anything else cannot occur. + _ => String::new(), + }; + out.push_str(&format!("\n#[test]\nfn {}() {{\n let mut h = harness(blank());\n {body}\n}}\n", test_name(&e.seq))); + } + out.push_str(&format!( + "\n#[test]\nfn generated_test_count_matches_the_table() {{\n const EXPECTED: usize = {};\n assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED);\n}}\n", + entries.len() + )); + out.push_str(SAMPLE_TEXT_FIRST); + out +} + +const HEADER: &str = r#"//! GENERATED from keytips.json by `cargo xtask keytips`. Edit the JSON, not this file. +//! +//! One end-to-end test per keytip sequence, through the real egui input path. +//! Assertions are generated from a probe of the live engine catalog (blank +//! workbook, empty params), so each test proves exactly what its command does +//! today: journaled, ran clean, surfaced a missing-params error, opened a +//! dialog, switched tabs, waited for more input, or stayed silent (pending). + +use egui::{Event, Key, Modifiers}; +use egui::os::OperatingSystem; +use gridcraft_engine::Session; +use gridcraft_ui_egui::SheetApp; + +type Harness = egui_kittest::Harness<'static, SheetApp>; + +fn harness(session: Session) -> Harness { + let mut h = egui_kittest::Harness::builder() + .with_os(OperatingSystem::Nix) + .with_size(egui::vec2(1200.0, 800.0)) + .build_ui_state( + |ui, app: &mut SheetApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SheetApp::new(session, Default::default()), + ); + h.run_steps(4); + h +} + +fn blank() -> Session { + let mut s = Session::new(); + s.new_workbook(); + s +} + +fn key_event(key: Key, pressed: bool, modifiers: Modifiers) -> Event { + Event::Key { key, physical_key: None, pressed, repeat: false, modifiers } +} + +fn events(h: &mut Harness, events: impl IntoIterator) { + h.input_mut().events.extend(events); + h.run_steps(2); +} + +fn key(h: &mut Harness, key: Key) { + events(h, [key_event(key, true, Modifiers::NONE), key_event(key, false, Modifiers::NONE)]); +} + +fn letter(h: &mut Harness, key: Key, text_first: bool) { + let text = Event::Text(key.name().to_ascii_lowercase()); + let pressed = key_event(key, true, Modifiers::NONE); + let pair = if text_first { [text, pressed] } else { [pressed, text] }; + events(h, pair.into_iter().chain([key_event(key, false, Modifiers::NONE)])); +} + +fn sequence(h: &mut Harness, seq: &str, text_first: bool) { + key(h, Key::F10); + for ch in seq.chars() { + let k = Key::from_name(&ch.to_string()).expect("sequence letters map to keys"); + letter(h, k, text_first); + } +} + +"#; + +const SAMPLE_TEXT_FIRST: &str = r#" +#[test] +fn sample_sequences_work_with_text_before_key() { + // Some IME paths deliver Event::Text ahead of the key press; the matcher + // must pair them either way. One representative of each action kind; + // the dialog opener runs last so it does not eat later input. + // Each step resets first: Escape twice leaves keytip mode so the next + // F10 arms cleanly (a lingering prefix would make F10 disarm instead). + let mut h = harness(blank()); + sequence(&mut h, "H", true); + assert_eq!(h.state().ui.ribbon_tab, "Home"); + reset(&mut h); + sequence(&mut h, "WVG", true); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines")); + reset(&mut h); + sequence(&mut h, "HV", true); + assert_eq!(h.state().keytips.prefix(), Some("HV")); + reset(&mut h); + sequence(&mut h, "NE", true); + assert!(h.state().dialog.is_none() && h.state().message.is_none() && h.state().toast.is_none()); + reset(&mut h); + sequence(&mut h, "HVS", true); + assert!(h.state().dialog.is_some(), "HVS must open the paste-special dialog"); +} + +fn reset(h: &mut Harness) { + key(h, Key::Escape); // clear any prefix, back to the armed root + key(h, Key::Escape); // leave keytip mode entirely +} +"#; From cbd9bb51c644e978d20082c77948bc3fe6b797a4 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 21:34:31 +0300 Subject: [PATCH 10/15] Dropdown keytips open their controls instead of firing empty commands; complete View badges - New Action::Control routes a keytip to its ribbon control (popup/dropdown open, checkbox toggle, button handler) instead of an engine command with empty params. Fixes ALT+H FF/FS/N/H/H error boxes. - Ribbon consumes a one-shot ui.keytip_invoke: font/size/number combos converted to the popup pattern (ComboBox has no open_memory), fill/font color menus force-open via SetOpenCommand, 100% zoom and Formula Bar run their handlers. - keytips.json: FF/FS/N/H/H -> control kind; View/Formulas/Data dialog openers (WQ zoom, HFM format cells, HO*, MV, AWG, sparkline types) and control WVF; MSP/HVP re-targeted; dropped 15 unreachable Home sub-items and non-Excel HWS shrink-to-fit. - View tab: badges on workbook views, gridlines/headings, formula bar. - Generated suite: 399 tests incl control-kind arm; direct tests 18. --- crates/ui-egui/src/keytips.rs | 4 + crates/ui-egui/src/keytips/keytip_table.rs | 4 +- .../src/keytips/keytip_table/generated.rs | 117 +++++----- crates/ui-egui/src/keytips/keytips.json | 207 +++++------------- crates/ui-egui/src/lib.rs | 12 + crates/ui-egui/src/ribbon.rs | 119 ++++++---- crates/ui-egui/tests/keytips.rs | 23 ++ crates/ui-egui/tests/keytips_generated.rs | 168 +++----------- xtask/src/keytips.rs | 11 +- 9 files changed, 257 insertions(+), 408 deletions(-) diff --git a/crates/ui-egui/src/keytips.rs b/crates/ui-egui/src/keytips.rs index 51a8793..e1163d9 100644 --- a/crates/ui-egui/src/keytips.rs +++ b/crates/ui-egui/src/keytips.rs @@ -14,6 +14,10 @@ pub enum Action { /// The target command does not exist yet: the sequence is reserved, its badge shows, and /// invoking it is a silent no-op until the command lands (see keytips.json `pending`). Pending(&'static str), + /// Route the keytip to the matching ribbon control: open its dropdown or run its click + /// handler (which carries the right params), instead of firing the engine command with + /// empty params. + Control(&'static str), } #[derive(Default)] diff --git a/crates/ui-egui/src/keytips/keytip_table.rs b/crates/ui-egui/src/keytips/keytip_table.rs index da59a2f..9e2735f 100644 --- a/crates/ui-egui/src/keytips/keytip_table.rs +++ b/crates/ui-egui/src/keytips/keytip_table.rs @@ -50,7 +50,7 @@ pub fn is_group_level(seq: &str) -> bool { fn matches_target(action: &Action, target: &str) -> bool { match action { - Action::Command(id) | Action::Pending(id) => *id == target, + Action::Command(id) | Action::Pending(id) | Action::Control(id) => *id == target, Action::Dialog(name) => format!("dialog:{name}") == target, Action::Tab(_) => false, } @@ -200,7 +200,7 @@ mod tests { #[test] fn item_letter_is_one_level_deep() { assert_eq!(item_letter("HV", "dialog:pasteSpecial"), Some("HVS")); - assert_eq!(item_letter("HV", "edit.pasteSpecial"), Some("HVA")); + assert_eq!(item_letter("HV", "edit.pasteSpecial"), Some("HVP")); assert_eq!(item_letter("H", "dialog:pasteSpecial"), None, "the item badge waits for its group letter"); assert_eq!(item_letter("HO", "home.autofitColumnWidth"), Some("HOI")); } diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index ee27773..ad2217c 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -72,16 +72,16 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HEF", Entry::Finish(Action::Command("edit.clearFormats"))), // Excel: Clear Formats ("HEL", Entry::Finish(Action::Command("edit.clearHyperlinks"))), // Excel: Clear Hyperlinks ("HEM", Entry::Finish(Action::Command("edit.clearComments"))), // Excel: Clear Comments and Notes; containment(Clear Comments and Notes) - ("HFC", Entry::Finish(Action::Command("home.fontColor"))), // Excel: Font Color + ("HFC", Entry::Finish(Action::Control("home.fontColor"))), // Excel: Font Color; opens the font-color palette ("HFDC", Entry::Finish(Action::Command("home.conditionalFormat"))), // Excel: Find Conditional Formatting; containment(Conditional Formatting) - ("HFDF", Entry::Finish(Action::Command("edit.find"))), // Excel: Find - ("HFDG", Entry::Finish(Action::Command("edit.goTo"))), // Excel: Go To + ("HFDF", Entry::Finish(Action::Dialog("find"))), // Excel: Find + ("HFDG", Entry::Finish(Action::Dialog("goTo"))), // Excel: Go To ("HFDM", Entry::Finish(Action::Command("edit.find"))), // Excel: Find Notes; containment(Find) ("HFDO", Entry::Finish(Action::Pending("home.selectObjects"))), // pending; Excel: Select Objects; Select Objects; reserved until the command lands ("HFDP", Entry::Finish(Action::Command("arrange.selectionPane"))), // Excel: Selection Pane - ("HFDR", Entry::Finish(Action::Command("edit.replace"))), // Excel: Replace + ("HFDR", Entry::Finish(Action::Dialog("find"))), // Excel: Replace; opens Find and Replace ("HFDS", Entry::Finish(Action::Command("edit.goToSpecial"))), // Excel: Go To Special - ("HFF", Entry::Finish(Action::Command("home.fontName"))), // Excel: Font Selection + ("HFF", Entry::Finish(Action::Control("home.fontName"))), // Excel: Font Selection; opens the font dropdown ("HFG", Entry::Finish(Action::Command("home.increaseFontSize"))), // Excel: Increase Font Size ("HFI", Entry::Continue), // Excel: Fill; group level: containment(Fill Up) ("HFIA", Entry::Finish(Action::Pending("home.fillAcrossWorksheets"))), // pending; Excel: Fill Across Worksheets; Fill Across Worksheets; reserved until the command lands @@ -93,13 +93,13 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HFIS", Entry::Finish(Action::Command("edit.fillSeries"))), // Excel: Fill Series; containment(Series) ("HFIU", Entry::Finish(Action::Command("edit.fillUp"))), // Excel: Fill Up ("HFK", Entry::Finish(Action::Command("home.decreaseFontSize"))), // Excel: Decrease Font Size - ("HFM", Entry::Finish(Action::Command("home.numberFormat"))), // Excel: More Number Formats; containment(Number Format) + ("HFM", Entry::Finish(Action::Dialog("formatCells"))), // Excel: More Number Formats; More Number Formats opens Format Cells ("HFP", Entry::Finish(Action::Command("edit.formatPainter"))), // Excel: Format Painter ("HFQ", Entry::Continue), // Excel: Orientation Menu; group level: containment(Orientation) ("HFQL", Entry::Finish(Action::Pending("home.angleClockwise"))), // pending; Excel: Angle Clockwise; Angle Clockwise; reserved until the command lands ("HFQO", Entry::Finish(Action::Pending("home.angleCounterclockwise"))), // pending; Excel: Angle Counterclockwise; Angle Counterclockwise; reserved until the command lands - ("HFS", Entry::Finish(Action::Command("home.fontSize"))), // Excel: Font Size - ("HH", Entry::Finish(Action::Command("home.fillColor"))), // Excel: Fill Color + ("HFS", Entry::Finish(Action::Control("home.fontSize"))), // Excel: Font Size; opens the font-size dropdown + ("HH", Entry::Finish(Action::Control("home.fillColor"))), // Excel: Fill Color; opens the fill-color palette ("HIC", Entry::Finish(Action::Command("home.insertColumns"))), // Excel: Insert Columns; Insert Columns ("HIE", Entry::Finish(Action::Command("home.insertCells"))), // Excel: Insert Copied Cells; Insert Cells (copied cells TBD) ("HII", Entry::Finish(Action::Command("home.insertCells"))), // Excel: Insert Cells @@ -123,15 +123,15 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HMC", Entry::Finish(Action::Command("home.mergeCenter"))), // Excel: Merge & Center ("HMM", Entry::Finish(Action::Command("home.mergeCells"))), // Excel: Merge Cells ("HMU", Entry::Finish(Action::Command("home.unmergeCells"))), // Excel: Unmerge Cells - ("HN", Entry::Finish(Action::Command("home.numberFormat"))), // Excel: Number Format + ("HN", Entry::Finish(Action::Control("home.numberFormat"))), // Excel: Number Format; opens the number-format dropdown ("HOA", Entry::Finish(Action::Command("home.autofitRowHeight"))), // Excel: AutoFit Row Height - ("HOD", Entry::Finish(Action::Command("home.columnWidth"))), // Excel: Default Column Width; containment(Column Width) + ("HOD", Entry::Finish(Action::Dialog("defaultWidth"))), // Excel: Default Column Width; containment(Column Width) ("HOE", Entry::Finish(Action::Command("home.formatCells"))), // Excel: Format Cells - ("HOH", Entry::Finish(Action::Command("home.rowHeight"))), // Excel: Row Height + ("HOH", Entry::Finish(Action::Dialog("rowHeight"))), // Excel: Row Height ("HOL", Entry::Finish(Action::Command("home.lockCell"))), // Excel: Lock Cell; containment(Lock Cell) - ("HOM", Entry::Finish(Action::Command("sheet.move"))), // Excel: Move or Copy Sheet + ("HOM", Entry::Finish(Action::Dialog("moveSheet"))), // Excel: Move or Copy Sheet ("HOP", Entry::Finish(Action::Command("review.protectSheet"))), // Excel: Protect Sheet - ("HOR", Entry::Finish(Action::Command("sheet.rename"))), // Excel: Rename Sheet + ("HOR", Entry::Finish(Action::Dialog("renameSheet"))), // Excel: Rename Sheet ("HOT", Entry::Finish(Action::Command("sheet.tabColor"))), // Excel: Tab Color ("HOUC", Entry::Finish(Action::Command("home.hideColumns"))), // Excel: Hide Columns ("HOUH", Entry::Finish(Action::Command("sheet.unhide"))), // Excel: Unhide Sheet @@ -139,7 +139,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HOUO", Entry::Finish(Action::Command("home.unhideRows"))), // Excel: Unhide Rows ("HOUR", Entry::Finish(Action::Command("home.hideRows"))), // Excel: Hide Rows ("HOUS", Entry::Finish(Action::Command("sheet.hide"))), // Excel: Hide Sheet - ("HOW", Entry::Finish(Action::Command("home.columnWidth"))), // Excel: Column Width + ("HOW", Entry::Finish(Action::Dialog("columnWidth"))), // Excel: Column Width ("HP", Entry::Finish(Action::Command("home.percent"))), // Excel: Excel: Percentage Style ("HSC", Entry::Finish(Action::Command("data.clearFilter"))), // Excel: Clear Filter ("HSF", Entry::Finish(Action::Command("data.filter"))), // Excel: Filter @@ -155,54 +155,39 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HUI", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Auto Min; AutoSum: Min (variant TBD) ("HUM", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Auto Max; AutoSum: Max (variant TBD) ("HV", Entry::Continue), // Excel: Paste; group level: - ("HVA", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Values and Number Formatting; paste variant, params TBD: what=TBD(values+numfmt) - ("HVB", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste No Borders; paste variant, params TBD: what=TBD(no-borders) - ("HVE", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Values and Source Formatting; paste variant, params TBD: what=TBD(values+src-fmt) - ("HVF", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Formula; paste-variant TBD - ("HVI", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Linked Picture; what=TBD(linked-picture) - ("HVK", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Keep Source Formatting; paste variant, params TBD: what=TBD(keep-source) - ("HVM", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Destination Formatting; paste variant, params TBD: what=TBD(dest) - ("HVN", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Link; paste variant, params TBD: what=TBD(link) - ("HVO", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Formula and Formatting; paste variant, params TBD: what=TBD(formula+formatting) - ("HVP", Entry::Finish(Action::Command("edit.paste"))), // Excel: Paste - ("HVR", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Formatting; paste variant, params TBD: what=formats - ("HVT", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Transpose; paste variant, params TBD: what=transpose - ("HVU", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Picture; what=TBD(picture) - ("HVV", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Values; paste variant, params TBD: what=values - ("HVW", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Keep Source Column Widths; paste variant, params TBD: what=TBD(col-widths) - ("HW", Entry::Continue), // Excel: Wrap Text Menu; group level: containment(Wrap Text) - ("HWS", Entry::Finish(Action::Command("home.shrinkToFit"))), // Excel: Shrink to Fit; landed in the catalog; live since T7 probe - ("HWW", Entry::Finish(Action::Command("home.wrapText"))), // Excel: Wrap Text - ("HX", Entry::Finish(Action::Command("edit.cut"))), // Excel: Cut - ("NC1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Column Chart; Column chart (kind TBD) + ("HVP", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste + ("HW", Entry::Continue), // Excel: Wrap Text Menu; group level: containment(Wrap Text) + ("HWW", Entry::Finish(Action::Command("home.wrapText"))), // Excel: Wrap Text + ("HX", Entry::Finish(Action::Command("edit.cut"))), // Excel: Cut + ("NC1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Column Chart; Column chart (kind TBD) ("NC2", Entry::Finish(Action::Command("review.newComment"))), // Excel: New Comment ("NCM", Entry::Finish(Action::Command("review.newComment"))), // Excel: Excel: New Comment (modern) - ("NCB", Entry::Finish(Action::Command("insert.checkbox"))), // Excel: Checkbox - ("ND", Entry::Finish(Action::Command("insert.chart"))), // Excel: Scatter Chart; Scatter (kind TBD) - ("NE", Entry::Finish(Action::Pending("insert.equation"))), // pending; Excel: Equations; Equations; reserved until the command lands - ("NF", Entry::Continue), // Excel: Online Pictures; group level: containment(Picture) - ("NFP", Entry::Continue), // Excel: From Picture Menu; group level: containment(Picture) - ("NFPF", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture From File; containment(Picture) + ("NCB", Entry::Finish(Action::Command("insert.checkbox"))), // Excel: Checkbox + ("ND", Entry::Finish(Action::Command("insert.chart"))), // Excel: Scatter Chart; Scatter (kind TBD) + ("NE", Entry::Finish(Action::Pending("insert.equation"))), // pending; Excel: Equations; Equations; reserved until the command lands + ("NF", Entry::Continue), // Excel: Online Pictures; group level: containment(Picture) + ("NFP", Entry::Continue), // Excel: From Picture Menu; group level: containment(Picture) + ("NFPF", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture From File; containment(Picture) ("NH1", Entry::Finish(Action::Command("pageLayout.headerFooter"))), // Excel: Header & Footer ("NHF", Entry::Finish(Action::Command("pageLayout.headerFooter"))), // Excel: Header & Footer - ("NHI", Entry::Finish(Action::Command("insert.chart"))), // Excel: Hierarchy Chart; Hierarchy (kind TBD) - ("NI1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Waterfall Chart; Waterfall (kind TBD) - ("NI2", Entry::Finish(Action::Command("insert.link"))), // Excel: Link - ("NJ", Entry::Finish(Action::Pending("insert.object"))), // pending; Excel: Object; insert.object missing; reserved until the command lands - ("NM1T", Entry::Finish(Action::Command("insert.picture"))), // Excel: SmartArt Picture; containment(Picture) - ("NM2", Entry::Finish(Action::Command("insert.chart"))), // Excel: Map Chart; Map (kind TBD) - ("NN1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Line Chart; Line chart (kind TBD) - ("NNS", Entry::Finish(Action::Command("insert.icons"))), // Excel: Icons - ("NP", Entry::Continue), // Excel: Picture; group level: - ("NPB", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from File; containment(Picture) - ("NPF", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place in Cell; Picture in cell (source TBD) - ("NPI", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place in Cell Menu; Picture in cell (source TBD) - ("NPO", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place over Cells Menu; Picture over cells (source TBD) - ("NQ", Entry::Finish(Action::Command("insert.chart"))), // Excel: Pie Chart; Pie (kind TBD) + ("NHI", Entry::Finish(Action::Command("insert.chart"))), // Excel: Hierarchy Chart; Hierarchy (kind TBD) + ("NI1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Waterfall Chart; Waterfall (kind TBD) + ("NI2", Entry::Finish(Action::Command("insert.link"))), // Excel: Link + ("NJ", Entry::Finish(Action::Pending("insert.object"))), // pending; Excel: Object; insert.object missing; reserved until the command lands + ("NM1T", Entry::Finish(Action::Command("insert.picture"))), // Excel: SmartArt Picture; containment(Picture) + ("NM2", Entry::Finish(Action::Command("insert.chart"))), // Excel: Map Chart; Map (kind TBD) + ("NN1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Line Chart; Line chart (kind TBD) + ("NNS", Entry::Finish(Action::Command("insert.icons"))), // Excel: Icons + ("NP", Entry::Continue), // Excel: Picture; group level: + ("NPB", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from File; containment(Picture) + ("NPF", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place in Cell; Picture in cell (source TBD) + ("NPI", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place in Cell Menu; Picture in cell (source TBD) + ("NPO", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place over Cells Menu; Picture over cells (source TBD) + ("NQ", Entry::Finish(Action::Command("insert.chart"))), // Excel: Pie Chart; Pie (kind TBD) ("NR", Entry::Finish(Action::Command("insert.recommendedCharts"))), // Excel: Recommended Charts - ("NSA", Entry::Finish(Action::Command("insert.chart"))), // Excel: Statistical Chart; Statistical chart (kind TBD) + ("NSA", Entry::Finish(Action::Command("insert.chart"))), // Excel: Statistical Chart; Statistical chart (kind TBD) ("NSC", Entry::Finish(Action::Pending("insert.screenshot"))), // pending; Excel: Screenshot Menu; Screenshot; reserved until the command lands - ("NSD", Entry::Finish(Action::Command("insert.chart"))), // Excel: Combo Chart; Combo (kind TBD) + ("NSD", Entry::Finish(Action::Command("insert.chart"))), // Excel: Combo Chart; Combo (kind TBD) ("NSF", Entry::Finish(Action::Pending("insert.slicer"))), // pending; Excel: Slicer; insert.slicer missing (gap #8); reserved until the command lands ("NSH", Entry::Finish(Action::Command("insert.shape"))), // Excel: Shapes ("NSP", Entry::Finish(Action::Command("insert.recommendedPivotTables"))), // Excel: Recommended PivotTables @@ -215,9 +200,9 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("NW", Entry::Finish(Action::Pending("insert.wordArt"))), // pending; Excel: WordArt; WordArt; reserved until the command lands ("NX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box ("NZK", Entry::Continue), // Excel: Sparklines Menu; group level: - ("NZKSL", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Line Sparkline - ("NZKSO", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Column Sparkline - ("NZKSW", Entry::Finish(Action::Command("insert.sparkline"))), // Excel: Win/Loss Sparkline + ("NZKSL", Entry::Finish(Action::Dialog("sparkline"))), // Excel: Line Sparkline + ("NZKSO", Entry::Finish(Action::Dialog("sparkline"))), // Excel: Column Sparkline + ("NZKSW", Entry::Finish(Action::Dialog("sparkline"))), // Excel: Win/Loss Sparkline ("NZTX", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Text Box ("NZT", Entry::Finish(Action::Command("insert.textBox"))), // Excel: Excel: Text Box (modern) ("PAE", Entry::Finish(Action::Command("arrange.sendBackward"))), // Excel: Send Backward @@ -278,7 +263,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("MR", Entry::Continue), // Excel: Recently Used Menu; group level: containment(Recently Used) ("MRF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Recently Used - Insert Function; containment(Insert Function) ("MS", Entry::Continue), // Excel: Use in Formula Menu; group level: containment(Use in Formula) - ("MSP", Entry::Finish(Action::Command("edit.pasteSpecial"))), // Excel: Paste Names; paste-variant TBD + ("MSP", Entry::Finish(Action::Pending("formulas.pasteName"))), // pending; Excel: Paste Names; reserved until the command lands ("MT", Entry::Continue), // Excel: Text Menu; group level: containment(Text) ("MTF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Text - Insert Function; containment(Insert Function) ("MU", Entry::Continue), // Excel: AutoSum Menu; group level: @@ -288,7 +273,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("MUI", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Min; AutoSum: Min (variant TBD) ("MUM", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Max; AutoSum: Max (variant TBD) ("MUS", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Sum - ("MV", Entry::Finish(Action::Command("formulas.evaluateFormula"))), // Excel: Evaluate Formula + ("MV", Entry::Finish(Action::Dialog("evaluateFormula"))), // Excel: Evaluate Formula ("MW", Entry::Finish(Action::Command("formulas.watchWindow"))), // Excel: Watch Window ("MX", Entry::Continue), // Excel: Calculation Options Menu; group level: containment(Calculation Options) ("MXA", Entry::Finish(Action::Command("formulas.calculationOptions"))), // Excel: Automatic Calculation; Calculation options: Automatic @@ -347,7 +332,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("AVR", Entry::Finish(Action::Pending("data.clearValidationCircles"))), // pending; Excel: Clear Validation Circles; reserved until the command lands ("AVV", Entry::Finish(Action::Command("data.validation"))), // Excel: Data Validation ("AW", Entry::Continue), // group: group: What-If (goalSeek/scenarioManager/dataTable) - ("AWG", Entry::Finish(Action::Command("data.goalSeek"))), // Excel: Goal Seek + ("AWG", Entry::Finish(Action::Dialog("goalSeek"))), // Excel: Goal Seek ("AWS", Entry::Finish(Action::Command("data.scenarioManager"))), // Excel: Scenario Manager ("AWT", Entry::Finish(Action::Command("data.dataTable"))), // Excel: Data Table ("AX", Entry::Finish(Action::Pending("data.existingConnections"))), // pending; Excel: Existing Connections; gap #12; reserved until the command lands @@ -377,21 +362,21 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("WG", Entry::Finish(Action::Command("view.zoomToSelection"))), // Excel: Zoom To Selection ("WH", Entry::Finish(Action::Command("view.hideWindow"))), // Excel: Hide Window ("WI", Entry::Finish(Action::Command("view.pageBreakPreview"))), // Excel: Page Break Preview - ("WJ", Entry::Finish(Action::Command("view.zoom"))), // Excel: Zoom To 100% + ("WJ", Entry::Finish(Action::Control("view.zoom"))), // Excel: Zoom To 100%; runs the 100% zoom handler (percent param) ("WL", Entry::Finish(Action::Command("view.normal"))), // Excel: Normal View; containment(Normal) ("WM", Entry::Continue), // Excel: Macros Menu; group level: containment(Macros) ("WMR", Entry::Finish(Action::Command("view.macros"))), // Excel: Record Macro; Record Macro (macros menu) ("WMV", Entry::Finish(Action::Command("view.macros"))), // Excel: View Macros; containment(Macros) ("WN", Entry::Finish(Action::Command("view.newWindow"))), // Excel: New Window ("WP", Entry::Finish(Action::Command("view.pageLayout"))), // Excel: Page Layout View - ("WQ", Entry::Finish(Action::Command("view.zoom"))), // Excel: Zoom + ("WQ", Entry::Finish(Action::Dialog("zoom"))), // Excel: Zoom; opens the Zoom dialog ("WR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler ("WS", Entry::Finish(Action::Command("view.split"))), // Excel: Split ("WT", Entry::Finish(Action::Pending("view.resetWindowPosition"))), // pending; Excel: Reset Window Position; reserved until the command lands ("WU", Entry::Finish(Action::Command("view.unhideWindow"))), // Excel: Unhide Window ("WVC", Entry::Finish(Action::Pending("view.focusCell"))), // pending; Excel: Focus Cell Menu; Focus Cell; reserved until the command lands - ("WVF", Entry::Finish(Action::Pending("view.formulaBar"))), // pending; Excel: Toggle Formula Bar; Toggle Formula Bar; reserved until the command lands - ("WVH", Entry::Finish(Action::Command("view.headings"))), // Excel: Toggle Headings; containment(Headings) + ("WVF", Entry::Finish(Action::Control("view.formulaBar"))), // Excel: Toggle Formula Bar; toggles the formula bar + ("WVH", Entry::Finish(Action::Command("view.headings"))), // Excel: Toggle Headings; containment(Headings) ("WVL", Entry::Finish(Action::Pending("view.synchronousScrolling"))), // pending; Excel: Synchronous Scrolling; Synchronous Scrolling; reserved until the command lands ("WVN", Entry::Finish(Action::Pending("view.navigation"))), // pending; Excel: Navigation; Navigation pane; reserved until the command lands ("WVR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index 555377b..18b6327 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -445,10 +445,10 @@ }, { "seq": "HFC", - "kind": "command", + "kind": "control", "target": "home.fontColor", "excel": "Font Color", - "note": "" + "note": "opens the font-color palette" }, { "seq": "HFDC", @@ -459,15 +459,15 @@ }, { "seq": "HFDF", - "kind": "command", - "target": "edit.find", + "kind": "dialog", + "target": "find", "excel": "Find", "note": "" }, { "seq": "HFDG", - "kind": "command", - "target": "edit.goTo", + "kind": "dialog", + "target": "goTo", "excel": "Go To", "note": "" }, @@ -495,10 +495,10 @@ }, { "seq": "HFDR", - "kind": "command", - "target": "edit.replace", + "kind": "dialog", + "target": "find", "excel": "Replace", - "note": "" + "note": "opens Find and Replace" }, { "seq": "HFDS", @@ -509,10 +509,10 @@ }, { "seq": "HFF", - "kind": "command", + "kind": "control", "target": "home.fontName", "excel": "Font Selection", - "note": "" + "note": "opens the font dropdown" }, { "seq": "HFG", @@ -593,10 +593,10 @@ }, { "seq": "HFM", - "kind": "command", - "target": "home.numberFormat", + "kind": "dialog", + "target": "formatCells", "excel": "More Number Formats", - "note": "containment(Number Format)" + "note": "More Number Formats opens Format Cells" }, { "seq": "HFP", @@ -629,17 +629,17 @@ }, { "seq": "HFS", - "kind": "command", + "kind": "control", "target": "home.fontSize", "excel": "Font Size", - "note": "" + "note": "opens the font-size dropdown" }, { "seq": "HH", - "kind": "command", + "kind": "control", "target": "home.fillColor", "excel": "Fill Color", - "note": "" + "note": "opens the fill-color palette" }, { "seq": "HIC", @@ -800,10 +800,10 @@ }, { "seq": "HN", - "kind": "command", + "kind": "control", "target": "home.numberFormat", "excel": "Number Format", - "note": "" + "note": "opens the number-format dropdown" }, { "seq": "HOA", @@ -814,8 +814,8 @@ }, { "seq": "HOD", - "kind": "command", - "target": "home.columnWidth", + "kind": "dialog", + "target": "defaultWidth", "excel": "Default Column Width", "note": "containment(Column Width)" }, @@ -828,8 +828,8 @@ }, { "seq": "HOH", - "kind": "command", - "target": "home.rowHeight", + "kind": "dialog", + "target": "rowHeight", "excel": "Row Height", "note": "" }, @@ -842,8 +842,8 @@ }, { "seq": "HOM", - "kind": "command", - "target": "sheet.move", + "kind": "dialog", + "target": "moveSheet", "excel": "Move or Copy Sheet", "note": "" }, @@ -856,8 +856,8 @@ }, { "seq": "HOR", - "kind": "command", - "target": "sheet.rename", + "kind": "dialog", + "target": "renameSheet", "excel": "Rename Sheet", "note": "" }, @@ -912,8 +912,8 @@ }, { "seq": "HOW", - "kind": "command", - "target": "home.columnWidth", + "kind": "dialog", + "target": "columnWidth", "excel": "Column Width", "note": "" }, @@ -1018,124 +1018,19 @@ "excel": "Paste", "note": "group level:" }, - { - "seq": "HVA", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Values and Number Formatting", - "note": "paste variant, params TBD: what=TBD(values+numfmt)" - }, - { - "seq": "HVB", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste No Borders", - "note": "paste variant, params TBD: what=TBD(no-borders)" - }, - { - "seq": "HVE", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Values and Source Formatting", - "note": "paste variant, params TBD: what=TBD(values+src-fmt)" - }, - { - "seq": "HVF", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Formula", - "note": "paste-variant TBD" - }, - { - "seq": "HVI", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Linked Picture", - "note": "what=TBD(linked-picture)" - }, - { - "seq": "HVK", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Keep Source Formatting", - "note": "paste variant, params TBD: what=TBD(keep-source)" - }, - { - "seq": "HVM", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Destination Formatting", - "note": "paste variant, params TBD: what=TBD(dest)" - }, - { - "seq": "HVN", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Link", - "note": "paste variant, params TBD: what=TBD(link)" - }, - { - "seq": "HVO", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Formula and Formatting", - "note": "paste variant, params TBD: what=TBD(formula+formatting)" - }, { "seq": "HVP", "kind": "command", - "target": "edit.paste", + "target": "edit.pasteSpecial", "excel": "Paste", "note": "" }, - { - "seq": "HVR", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Formatting", - "note": "paste variant, params TBD: what=formats" - }, - { - "seq": "HVT", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Transpose", - "note": "paste variant, params TBD: what=transpose" - }, - { - "seq": "HVU", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Picture", - "note": "what=TBD(picture)" - }, - { - "seq": "HVV", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Values", - "note": "paste variant, params TBD: what=values" - }, - { - "seq": "HVW", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste Keep Source Column Widths", - "note": "paste variant, params TBD: what=TBD(col-widths)" - }, { "seq": "HW", "kind": "continue", "excel": "Wrap Text Menu", "note": "group level: containment(Wrap Text)" }, - { - "seq": "HWS", - "kind": "command", - "target": "home.shrinkToFit", - "excel": "Shrink to Fit", - "note": "landed in the catalog; live since T7 probe" - }, { "seq": "HWW", "kind": "command", @@ -1440,22 +1335,22 @@ }, { "seq": "NZKSL", - "kind": "command", - "target": "insert.sparkline", + "kind": "dialog", + "target": "sparkline", "excel": "Line Sparkline", "note": "" }, { "seq": "NZKSO", - "kind": "command", - "target": "insert.sparkline", + "kind": "dialog", + "target": "sparkline", "excel": "Column Sparkline", "note": "" }, { "seq": "NZKSW", - "kind": "command", - "target": "insert.sparkline", + "kind": "dialog", + "target": "sparkline", "excel": "Win/Loss Sparkline", "note": "" }, @@ -1867,9 +1762,10 @@ { "seq": "MSP", "kind": "command", - "target": "edit.pasteSpecial", + "target": "formulas.pasteName", "excel": "Paste Names", - "note": "paste-variant TBD" + "note": "reserved until the command lands", + "pending": true }, { "seq": "MT", @@ -1934,8 +1830,8 @@ }, { "seq": "MV", - "kind": "command", - "target": "formulas.evaluateFormula", + "kind": "dialog", + "target": "evaluateFormula", "excel": "Evaluate Formula", "note": "" }, @@ -2355,8 +2251,8 @@ }, { "seq": "AWG", - "kind": "command", - "target": "data.goalSeek", + "kind": "dialog", + "target": "goalSeek", "excel": "Goal Seek", "note": "" }, @@ -2572,10 +2468,10 @@ }, { "seq": "WJ", - "kind": "command", + "kind": "control", "target": "view.zoom", "excel": "Zoom To 100%", - "note": "" + "note": "runs the 100% zoom handler (percent param)" }, { "seq": "WL", @@ -2620,10 +2516,10 @@ }, { "seq": "WQ", - "kind": "command", - "target": "view.zoom", + "kind": "dialog", + "target": "zoom", "excel": "Zoom", - "note": "" + "note": "opens the Zoom dialog" }, { "seq": "WR", @@ -2664,11 +2560,10 @@ }, { "seq": "WVF", - "kind": "command", + "kind": "control", "target": "view.formulaBar", - "pending": true, "excel": "Toggle Formula Bar", - "note": "Toggle Formula Bar; reserved until the command lands" + "note": "toggles the formula bar" }, { "seq": "WVH", diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index b62bebc..ee75d6a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -57,6 +57,10 @@ pub struct UiState { /// Motion in the UI: the selection outline glides to a new selection (View ▸ Animations). pub animations: bool, pub recent: Vec, + /// One-shot keytip request for a ribbon control: set when a keytip action resolves to + /// [`keytips::Action::Control`], consumed (and cleared) by the control's render site. + #[serde(skip)] + pub keytip_invoke: Option<&'static str>, /// Interface language. English by default (so tests and headless renders never depend on the /// host's locale); the desktop app starts from [`i18n::Language::system`] when `ui.json` has /// no usable language (see [`i18n::saved_language`]). An unknown saved value reads as English @@ -95,6 +99,7 @@ impl Default for UiState { status_bar: true, animations: true, recent: vec![], + keytip_invoke: None, language: i18n::Language::En, interface_scale: 100, } @@ -821,6 +826,9 @@ impl SheetApp { /// Per-frame logic (control channel, screenshots). Call before `ui`. pub fn logic(&mut self, ctx: &egui::Context) { + // A keytip control request lives one frame: the ribbon consumes it during `ui`, and + // anything that never reached a control (collapsed ribbon, tab switch) is dropped. + self.ui.keytip_invoke = None; if self.fonts_han.is_some_and(|han| han != theme::HanOrder::of(self.ui.language)) { self.fonts_ready = false; self.fonts_set = false; @@ -1022,6 +1030,10 @@ impl SheetApp { self.run_or_alert(id, json!({})); } } + // Route to the ribbon control that owns this keytip: it opens its dropdown or + // runs its click handler with the right params (the engine command alone would + // be missing them). + keytips::Action::Control(id) => self.ui.keytip_invoke = Some(id), } } } diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 0afc33a..83e448b 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -343,6 +343,21 @@ fn anchor_keytip(app: &SheetApp, ui: &Ui, response: &egui::Response, targets: &[ } } +/// Consume a one-shot keytip request for control `id` (set by `Action::Control`). +fn invoke(app: &mut SheetApp, id: &str) -> bool { + if app.ui.keytip_invoke == Some(id) { + app.ui.keytip_invoke = None; + true + } else { + false + } +} + +/// Force-open command for a popup: either a keytip invoke (open it) or a click (toggle). +fn popup_command(forced: bool, clicked: bool) -> Option { + if forced { Some(egui::SetOpenCommand::Bool(true)) } else { clicked.then_some(egui::SetOpenCommand::Toggle) } +} + fn menu_items(app: &mut SheetApp, ui: &mut Ui, items: &[(&str, &str, serde_json::Value)]) { for (label, id, p) in items { if *label == "-" { @@ -534,7 +549,8 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { if f { act(app, "home.fillColor", json!({"color": fill})); } - egui::Popup::menu(&fa).show(|ui| { + let forced = invoke(app, "home.fillColor"); + egui::Popup::menu(&fa).open_memory(popup_command(forced, fa.clicked())).show(|ui| { if let Some(c) = color_palette(ui, &theme_colors(app), "No Fill") { if c != "none" { app.grid.last_fill = c.clone(); @@ -550,7 +566,8 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { if c { act(app, "home.fontColor", json!({"color": fc})); } - egui::Popup::menu(&ca).show(|ui| { + let forced = invoke(app, "home.fontColor"); + egui::Popup::menu(&ca).open_memory(popup_command(forced, ca.clicked())).show(|ui| { if let Some(c) = color_palette(ui, &theme_colors(app), "Automatic") { if c != "none" { app.grid.last_font_color = c.clone(); @@ -929,16 +946,15 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { fn font_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { let mut name = st.font.name.clone(); let before = name.clone(); - let resp = egui::ComboBox::from_id_salt("font_name").width(150.0).selected_text(egui::RichText::new(&name).font(theme::ui_font(12.5))).show_ui( - ui, - |ui| { - for f in FONTS { - let fam = theme::cell_family(f, false, false); - ui.selectable_value(&mut name, f.to_string(), egui::RichText::new(*f).font(egui::FontId::new(14.0, fam))); - } - }, - ); - control_keytip(app, ui, &resp.response, "home.fontName"); + let resp = ui.add_sized([150.0, 24.0], egui::Button::new(egui::RichText::new(&name).font(theme::ui_font(12.5)))); + control_keytip(app, ui, &resp, "home.fontName"); + let forced = invoke(app, "home.fontName"); + egui::Popup::menu(&resp).open_memory(popup_command(forced, resp.clicked())).show(|ui| { + for f in FONTS { + let fam = theme::cell_family(f, false, false); + ui.selectable_value(&mut name, f.to_string(), egui::RichText::new(*f).font(egui::FontId::new(14.0, fam))); + } + }); if name != before { act(app, "home.fontName", json!({"name": name})); } @@ -968,15 +984,14 @@ fn size_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { let mut size = st.font.size; let before = size; let label = if size.fract() == 0.0 { format!("{}", size as i32) } else { format!("{size}") }; - let resp = egui::ComboBox::from_id_salt("font_size").width(52.0).selected_text(egui::RichText::new(label).font(theme::ui_font(12.5))).show_ui( - ui, - |ui| { - for s in [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0, 48.0, 72.0] { - ui.selectable_value(&mut size, s, format!("{}", s as i32)); - } - }, - ); - control_keytip(app, ui, &resp.response, "home.fontSize"); + let resp = ui.add_sized([52.0, 24.0], egui::Button::new(egui::RichText::new(label).font(theme::ui_font(12.5)))); + control_keytip(app, ui, &resp, "home.fontSize"); + let forced = invoke(app, "home.fontSize"); + egui::Popup::menu(&resp).open_memory(popup_command(forced, resp.clicked())).show(|ui| { + for s in [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0, 48.0, 72.0] { + ui.selectable_value(&mut size, s, format!("{}", s as i32)); + } + }); if size != before { act(app, "home.fontSize", json!({"size": size})); } @@ -989,26 +1004,24 @@ fn number_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { let lang = app.ui.language; let sample = app.session.active().and_then(|d| d.wb.active().map(|sh| sh.value(d.selection.active))).unwrap_or_default(); let mut picked: Option<&str> = None; - let resp = egui::ComboBox::from_id_salt("number_format") - .width(150.0) - .selected_text(egui::RichText::new(lang.tr(¤t)).font(theme::ui_font(12.5))) - .show_ui(ui, |ui| { - for name in - ["General", "Number", "Currency", "Accounting", "Short Date", "Long Date", "Time", "Percentage", "Fraction", "Scientific", "Text"] - { - let code = gridcraft_engine::cmd::format::format_code_for(name); - let preview = app.session.active().map(|d| gridcraft_engine::display::format(&sample, code, &d.wb).text).unwrap_or_default(); - let r = ui.add(egui::Button::selectable(current == name, format!("{:<12} {preview}", lang.tr(name))).min_size(vec2(240.0, 22.0))); - if r.clicked() { - picked = Some(name); - } + let resp = ui.add_sized([150.0, 24.0], egui::Button::new(egui::RichText::new(lang.tr(¤t)).font(theme::ui_font(12.5)))); + control_keytip(app, ui, &resp, "home.numberFormat"); + let forced = invoke(app, "home.numberFormat"); + egui::Popup::menu(&resp).open_memory(popup_command(forced, resp.clicked())).show(|ui| { + for name in ["General", "Number", "Currency", "Accounting", "Short Date", "Long Date", "Time", "Percentage", "Fraction", "Scientific", "Text"] + { + let code = gridcraft_engine::cmd::format::format_code_for(name); + let preview = app.session.active().map(|d| gridcraft_engine::display::format(&sample, code, &d.wb).text).unwrap_or_default(); + let r = ui.add(egui::Button::selectable(current == name, format!("{:<12} {preview}", lang.tr(name))).min_size(vec2(240.0, 22.0))); + if r.clicked() { + picked = Some(name); } - ui.separator(); - if ui.button(lang.tr("More Number Formats…")).clicked() { - picked = Some("__more"); - } - }); - anchor_keytip(app, ui, &resp.response, &["home.numberFormat"]); + } + ui.separator(); + if ui.button(lang.tr("More Number Formats…")).clicked() { + picked = Some("__more"); + } + }); match picked { Some("__more") => app.open_dialog("formatCells", json!({"tab": "Number"})), Some(n) => act(app, "home.numberFormat", json!({"format": n})), @@ -1657,7 +1670,7 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { app.open_dialog("defineName", json!({})); } let use_in = small_button(ui, Icon::Function, "Use in Formula", "Use in Formula", true); - anchor_keytip(app, ui, &use_in, &["edit.pasteSpecial"]); + anchor_keytip(app, ui, &use_in, &["formulas.useInFormula"]); egui::Popup::menu(&use_in).show(|ui| { let names: Vec = app.session.active().map(|d| d.wb.names.iter().map(|n| n.name.clone()).collect()).unwrap_or_default(); for n in names { @@ -1965,7 +1978,9 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { (Icon::PageBreak, "Page Break\nPreview", "view.pageBreakPreview"), (Icon::PageLayout, "Page\nLayout", "view.pageLayout"), ] { - if big_button(ui, icon, label, label, false).clicked() { + let __k = big_button(ui, icon, label, label, false); + control_keytip(app, ui, &__k, cmd); + if __k.clicked() { act(app, cmd, json!({})); } } @@ -1977,19 +1992,27 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { ui.vertical(|ui| { // Rulers exist only in Page Layout view. let mut ruler = !app.session.hide_ruler; - if ui.add_enabled(page_layout, egui::Checkbox::new(&mut ruler, lang.tr("Ruler"))).changed() { + let ruler_resp = ui.add_enabled(page_layout, egui::Checkbox::new(&mut ruler, lang.tr("Ruler"))); + control_keytip(app, ui, &ruler_resp, "view.ruler"); + if ruler_resp.changed() { act(app, "view.ruler", json!({"on": ruler})); } let mut fb = app.session.prefs.formula_bar; - if ui.checkbox(&mut fb, lang.tr("Formula Bar")).changed() { - act(app, "view.formulaBar", json!({"on": fb})); + let resp = ui.checkbox(&mut fb, lang.tr("Formula Bar")); + control_keytip(app, ui, &resp, "view.formulaBar"); + if resp.changed() || invoke(app, "view.formulaBar") { + act(app, "view.formulaBar", json!({"on": !fb})); } let mut g = s.0; - if ui.checkbox(&mut g, lang.tr("Gridlines")).changed() { + let resp = ui.checkbox(&mut g, lang.tr("Gridlines")); + control_keytip(app, ui, &resp, "view.gridlines"); + if resp.changed() { act(app, "view.gridlines", json!({"on": g})); } let mut h = s.1; - if ui.checkbox(&mut h, lang.tr("Headings")).changed() { + let resp = ui.checkbox(&mut h, lang.tr("Headings")); + control_keytip(app, ui, &resp, "view.headings"); + if resp.changed() { act(app, "view.headings", json!({"on": h})); } }); @@ -2001,7 +2024,7 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { } let __k = big_button(ui, Icon::Search, "100%", "Zoom to 100%", false); control_keytip(app, ui, &__k, "view.zoom"); - if __k.clicked() { + if __k.clicked() || invoke(app, "view.zoom") { act(app, "view.zoom", json!({"percent": 100})); } let __k = big_button(ui, Icon::Zoom, "Zoom to\nSelection", "Zoom to Selection", false); diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index 35b3e1e..a045593 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -433,6 +433,29 @@ fn pending_sequences_go_live_when_the_command_lands() { assert_eq!(h.state().keytips.prefix(), None); } +#[test] +fn control_keytips_open_dropdowns_and_run_handlers() { + // HFF routes to the font control: its dropdown opens (popup items queryable). + let mut h = harness(blank()); + sequence(&mut h, &[Key::H, Key::F, Key::F], false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HFF must not raise an error box"); + // "Calibri" appears twice: once on the combo button (the current font) and + // once inside the opened dropdown — a single node would mean it stayed closed. + assert!(h.query_all_by_label("Calibri").count() >= 2, "HFF must open the font dropdown"); + + // WVF toggles the formula bar: pure UI state, no engine command exists. + let mut h = harness(blank()); + assert!(h.state().ui.formula_bar); + sequence(&mut h, &[Key::W, Key::V, Key::F], false); + assert!(!h.state().ui.formula_bar, "WVF must toggle the formula bar off"); + + // WJ runs the 100% handler with its percent param ({} would raise "missing percent"). + let mut h = harness(blank()); + sequence(&mut h, &[Key::W, Key::J], false); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WJ must run view.zoom with the percent param"); + assert_eq!(h.state().keytips.prefix(), None); +} + #[test] fn every_sequence_terminates_without_panicking() { use gridcraft_ui_egui::keytips::keytip_table::SEQUENCES; diff --git a/crates/ui-egui/tests/keytips_generated.rs b/crates/ui-egui/tests/keytips_generated.rs index bcc3bdf..6e52dc9 100644 --- a/crates/ui-egui/tests/keytips_generated.rs +++ b/crates/ui-egui/tests/keytips_generated.rs @@ -543,7 +543,8 @@ fn keytip_hem_() { fn keytip_hfc_() { let mut h = harness(blank()); sequence(&mut h, "HFC", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFC must invoke home.fontColor and surface its missing-params error"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HFC must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -557,14 +558,14 @@ fn keytip_hfdc_() { fn keytip_hfdf_() { let mut h = harness(blank()); sequence(&mut h, "HFDF", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDF must invoke edit.find and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HFDF must open the find dialog"); } #[test] fn keytip_hfdg_() { let mut h = harness(blank()); sequence(&mut h, "HFDG", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDG must invoke edit.goTo and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HFDG must open the goTo dialog"); } #[test] @@ -595,7 +596,7 @@ fn keytip_hfdp_() { fn keytip_hfdr_() { let mut h = harness(blank()); sequence(&mut h, "HFDR", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFDR must invoke edit.replace and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HFDR must open the find dialog"); } #[test] @@ -609,7 +610,8 @@ fn keytip_hfds_() { fn keytip_hff_() { let mut h = harness(blank()); sequence(&mut h, "HFF", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFF must invoke home.fontName and surface its missing-params error"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HFF must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -696,7 +698,7 @@ fn keytip_hfk_() { fn keytip_hfm_() { let mut h = harness(blank()); sequence(&mut h, "HFM", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFM must invoke home.numberFormat and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HFM must open the formatCells dialog"); } #[test] @@ -737,14 +739,16 @@ fn keytip_hfqo_() { fn keytip_hfs_() { let mut h = harness(blank()); sequence(&mut h, "HFS", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HFS must invoke home.fontSize and surface its missing-params error"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HFS must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] fn keytip_hh_() { let mut h = harness(blank()); sequence(&mut h, "HH", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HH must invoke home.fillColor and surface its missing-params error"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HH must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -912,7 +916,8 @@ fn keytip_hmu_() { fn keytip_hn_() { let mut h = harness(blank()); sequence(&mut h, "HN", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HN must invoke home.numberFormat and surface its missing-params error"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HN must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -926,7 +931,7 @@ fn keytip_hoa_() { fn keytip_hod_() { let mut h = harness(blank()); sequence(&mut h, "HOD", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOD must invoke home.columnWidth and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HOD must open the defaultWidth dialog"); } #[test] @@ -940,7 +945,7 @@ fn keytip_hoe_() { fn keytip_hoh_() { let mut h = harness(blank()); sequence(&mut h, "HOH", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOH must invoke home.rowHeight and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HOH must open the rowHeight dialog"); } #[test] @@ -954,7 +959,7 @@ fn keytip_hol_() { fn keytip_hom_() { let mut h = harness(blank()); sequence(&mut h, "HOM", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOM must invoke sheet.move and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HOM must open the moveSheet dialog"); } #[test] @@ -968,7 +973,7 @@ fn keytip_hop_() { fn keytip_hor_() { let mut h = harness(blank()); sequence(&mut h, "HOR", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOR must invoke sheet.rename and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HOR must open the renameSheet dialog"); } #[test] @@ -1024,7 +1029,7 @@ fn keytip_hous_() { fn keytip_how_() { let mut h = harness(blank()); sequence(&mut h, "HOW", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HOW must invoke home.columnWidth and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "HOW must open the columnWidth dialog"); } #[test] @@ -1132,109 +1137,11 @@ fn keytip_hv_() { assert_eq!(h.state().keytips.prefix(), Some("HV"), "HV must stay armed waiting for more input"); } -#[test] -fn keytip_hva_() { - let mut h = harness(blank()); - sequence(&mut h, "HVA", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVA must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvb_() { - let mut h = harness(blank()); - sequence(&mut h, "HVB", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVB must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hve_() { - let mut h = harness(blank()); - sequence(&mut h, "HVE", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVE must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvf_() { - let mut h = harness(blank()); - sequence(&mut h, "HVF", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVF must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvi_() { - let mut h = harness(blank()); - sequence(&mut h, "HVI", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVI must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvk_() { - let mut h = harness(blank()); - sequence(&mut h, "HVK", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVK must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvm_() { - let mut h = harness(blank()); - sequence(&mut h, "HVM", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVM must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvn_() { - let mut h = harness(blank()); - sequence(&mut h, "HVN", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVN must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvo_() { - let mut h = harness(blank()); - sequence(&mut h, "HVO", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVO must invoke edit.pasteSpecial and surface its missing-params error"); -} - #[test] fn keytip_hvp_() { let mut h = harness(blank()); sequence(&mut h, "HVP", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVP must invoke edit.paste and surface its missing-params error"); -} - -#[test] -fn keytip_hvr_() { - let mut h = harness(blank()); - sequence(&mut h, "HVR", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVR must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvt_() { - let mut h = harness(blank()); - sequence(&mut h, "HVT", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVT must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvu_() { - let mut h = harness(blank()); - sequence(&mut h, "HVU", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVU must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvv_() { - let mut h = harness(blank()); - sequence(&mut h, "HVV", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVV must invoke edit.pasteSpecial and surface its missing-params error"); -} - -#[test] -fn keytip_hvw_() { - let mut h = harness(blank()); - sequence(&mut h, "HVW", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVW must invoke edit.pasteSpecial and surface its missing-params error"); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "HVP must invoke edit.pasteSpecial and surface its missing-params error"); } #[test] @@ -1244,13 +1151,6 @@ fn keytip_hw_() { assert_eq!(h.state().keytips.prefix(), Some("HW"), "HW must stay armed waiting for more input"); } -#[test] -fn keytip_hws_() { - let mut h = harness(blank()); - sequence(&mut h, "HWS", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.shrinkToFit"), "HWS must journal home.shrinkToFit"); -} - #[test] fn keytip_hww_() { let mut h = harness(blank()); @@ -1580,21 +1480,21 @@ fn keytip_nzk_() { fn keytip_nzksl_() { let mut h = harness(blank()); sequence(&mut h, "NZKSL", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSL must invoke insert.sparkline and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "NZKSL must open the sparkline dialog"); } #[test] fn keytip_nzkso_() { let mut h = harness(blank()); sequence(&mut h, "NZKSO", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSO must invoke insert.sparkline and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "NZKSO must open the sparkline dialog"); } #[test] fn keytip_nzksw_() { let mut h = harness(blank()); sequence(&mut h, "NZKSW", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "NZKSW must invoke insert.sparkline and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "NZKSW must open the sparkline dialog"); } #[test] @@ -2024,7 +1924,10 @@ fn keytip_ms_() { fn keytip_msp_() { let mut h = harness(blank()); sequence(&mut h, "MSP", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "MSP must invoke edit.pasteSpecial and surface its missing-params error"); + assert!(h.state().dialog.is_none(), "MSP must not open a dialog"); + assert!(h.state().message.is_none(), "MSP must not open a message box"); + assert!(h.state().toast.is_none(), "MSP must not raise a toast"); + assert!(h.state().session.journal.is_empty(), "MSP must not journal"); } #[test] @@ -2094,7 +1997,7 @@ fn keytip_mus_() { fn keytip_mv_() { let mut h = harness(blank()); sequence(&mut h, "MV", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "MV must invoke formulas.evaluateFormula and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "MV must open the evaluateFormula dialog"); } #[test] @@ -2567,7 +2470,7 @@ fn keytip_aw_() { fn keytip_awg_() { let mut h = harness(blank()); sequence(&mut h, "AWG", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "AWG must invoke data.goalSeek and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "AWG must open the goalSeek dialog"); } #[test] @@ -2804,7 +2707,8 @@ fn keytip_wi_() { fn keytip_wj_() { let mut h = harness(blank()); sequence(&mut h, "WJ", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "WJ must invoke view.zoom and surface its missing-params error"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WJ must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -2853,7 +2757,7 @@ fn keytip_wp_() { fn keytip_wq_() { let mut h = harness(blank()); sequence(&mut h, "WQ", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "WQ must invoke view.zoom and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "WQ must open the zoom dialog"); } #[test] @@ -2901,10 +2805,8 @@ fn keytip_wvc_() { fn keytip_wvf_() { let mut h = harness(blank()); sequence(&mut h, "WVF", false); - assert!(h.state().dialog.is_none(), "WVF must not open a dialog"); - assert!(h.state().message.is_none(), "WVF must not open a message box"); - assert!(h.state().toast.is_none(), "WVF must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "WVF must not journal"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "WVF must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -3131,7 +3033,7 @@ fn keytip_9_() { #[test] fn generated_test_count_matches_the_table() { - const EXPECTED: usize = 412; + const EXPECTED: usize = 397; assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED); } diff --git a/xtask/src/keytips.rs b/xtask/src/keytips.rs index 4e3c1dd..3684722 100644 --- a/xtask/src/keytips.rs +++ b/xtask/src/keytips.rs @@ -138,7 +138,7 @@ fn parse_and_probe(source: &str) -> Result<(Vec, Probe), String> { return Err(format!("{at}: tab target {target:?} is not one of {TABS:?}")); } } - "command" | "dialog" => { + "command" | "dialog" | "control" => { if target.is_empty() { return Err(format!("{at}: {kind} needs a `target`")); } @@ -148,10 +148,10 @@ fn parse_and_probe(source: &str) -> Result<(Vec, Probe), String> { } // A terminal Finish entry on a strict prefix makes the longer sequence // unreachable (the prefix fires and clears first), e.g. HC (Copy) kills HCP. - if kind == "command" || kind == "dialog" { + if matches!(kind.as_str(), "command" | "dialog" | "control") { for cut in 1..seq.len() { let prefix = &seq[..cut]; - if entries.iter().any(|e| e.seq == prefix && (e.kind == "command" || e.kind == "dialog")) { + if entries.iter().any(|e| e.seq == prefix && matches!(e.kind.as_str(), "command" | "dialog" | "control")) { return Err(format!( "{at}: seq {seq:?} is unreachable — {prefix:?} already fires first; the deeper item needs a menu (item-letter), not a trie sequence" )); @@ -221,6 +221,7 @@ fn render_table(entries: &[Entry]) -> String { "command" if e.pending => format!("Entry::Finish(Action::Pending({:?}))", e.target), "command" => format!("Entry::Finish(Action::Command({:?}))", e.target), "dialog" => format!("Entry::Finish(Action::Dialog({:?}))", e.target), + "control" => format!("Entry::Finish(Action::Control({:?}))", e.target), "continue" => "Entry::Continue".to_string(), // All kinds are validated during parse; anything else cannot occur. _ => String::new(), @@ -290,6 +291,10 @@ fn render_tests(entries: &[Entry], probe: &Probe) -> String { seq = e.seq, dialog = e.target, ), + "control" => format!( + "sequence(&mut h, {seq:?}, false);\n assert!(h.state().message.is_none() && h.state().toast.is_none(), \"{seq} must route to its control without an error box\");\n assert_eq!(h.state().keytips.prefix(), None);", + seq = e.seq, + ), "continue" => format!( "sequence(&mut h, {seq:?}, false);\n assert_eq!(h.state().keytips.prefix(), Some({seq:?}), \"{seq} must stay armed waiting for more input\");", seq = e.seq, From 7ba871c6f2e1933c44a805b2dd0f6e62b04ba922 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 22:01:46 +0300 Subject: [PATCH 11/15] Excel-style combo navigation (type to filter, arrows to move) and complete tab badges - editable_combo: every dropdown keytip opens with a filter field focused; typing narrows the list, Up/Down move the highlight (wrapping), Enter or click applies, custom text applies where valid (font size). Fixes arrows and typing in ALT+H FF/FS/N. - Audited all 54 painted ribbon ids against the table: real mismatches fixed (Data Sort/Text-to-Columns/Remove-Duplicates/Subtotal, Review Spelling/ Accessibility/New Comment/Notes, View Zoom/Freeze, Formulas Insert Function/ Name Manager/Define Name/Use-in-Formula/Error Checking/Evaluate, Insert Link/ Comment/Header-Footer/Sparklines, Home Protect Sheet). - keytips.json: dialog kind for real dialog openers; control kind for the 6 chart-type buttons, 7 formula-category buttons, and Orientation (HFQ added, was missing); M-family restructured to Excel semantics (ML/MT/ME/MO/MG/MQ fire directly, MI1 opens Financial) with 14 unreachable children dropped; 383 entries total. --- .../src/keytips/keytip_table/generated.rs | 130 ++++----- crates/ui-egui/src/keytips/keytips.json | 264 ++++++------------ crates/ui-egui/src/ribbon.rs | 264 ++++++++++++------ crates/ui-egui/tests/keytips.rs | 41 +++ crates/ui-egui/tests/keytips_generated.rs | 206 ++++---------- 5 files changed, 406 insertions(+), 499 deletions(-) diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index ad2217c..479e5bc 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -64,7 +64,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HBY", Entry::Finish(Action::Command("home.borders"))), // Excel: Line Style; Line Style (submenu TBD) ("HC", Entry::Finish(Action::Command("edit.copy"))), // Excel: Copy; fires immediately in Excel; deeper aliases dropped as unreachable ("HDC", Entry::Finish(Action::Command("home.deleteColumns"))), // Excel: Delete Columns; Delete Columns - ("HDD", Entry::Finish(Action::Command("home.deleteCells"))), // Excel: Delete Cells + ("HDD", Entry::Finish(Action::Dialog("deleteCells"))), // Excel: Delete Cells ("HDR", Entry::Finish(Action::Command("home.deleteRows"))), // Excel: Delete Rows; Delete Rows ("HDS", Entry::Finish(Action::Command("home.deleteSheet"))), // Excel: Delete Sheet ("HEA", Entry::Finish(Action::Command("edit.clearAll"))), // Excel: Clear All @@ -80,7 +80,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HFDO", Entry::Finish(Action::Pending("home.selectObjects"))), // pending; Excel: Select Objects; Select Objects; reserved until the command lands ("HFDP", Entry::Finish(Action::Command("arrange.selectionPane"))), // Excel: Selection Pane ("HFDR", Entry::Finish(Action::Dialog("find"))), // Excel: Replace; opens Find and Replace - ("HFDS", Entry::Finish(Action::Command("edit.goToSpecial"))), // Excel: Go To Special + ("HFDS", Entry::Finish(Action::Dialog("goToSpecial"))), // Excel: Go To Special ("HFF", Entry::Finish(Action::Control("home.fontName"))), // Excel: Font Selection; opens the font dropdown ("HFG", Entry::Finish(Action::Command("home.increaseFontSize"))), // Excel: Increase Font Size ("HFI", Entry::Continue), // Excel: Fill; group level: containment(Fill Up) @@ -90,24 +90,22 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HFIJ", Entry::Finish(Action::Command("home.justify"))), // Excel: Fill Justify; containment(Justify) ("HFIL", Entry::Finish(Action::Command("edit.fillLeft"))), // Excel: Fill Left ("HFIR", Entry::Finish(Action::Command("edit.fillRight"))), // Excel: Fill Right - ("HFIS", Entry::Finish(Action::Command("edit.fillSeries"))), // Excel: Fill Series; containment(Series) + ("HFIS", Entry::Finish(Action::Dialog("series"))), // Excel: Fill Series; containment(Series) ("HFIU", Entry::Finish(Action::Command("edit.fillUp"))), // Excel: Fill Up ("HFK", Entry::Finish(Action::Command("home.decreaseFontSize"))), // Excel: Decrease Font Size ("HFM", Entry::Finish(Action::Dialog("formatCells"))), // Excel: More Number Formats; More Number Formats opens Format Cells ("HFP", Entry::Finish(Action::Command("edit.formatPainter"))), // Excel: Format Painter - ("HFQ", Entry::Continue), // Excel: Orientation Menu; group level: containment(Orientation) - ("HFQL", Entry::Finish(Action::Pending("home.angleClockwise"))), // pending; Excel: Angle Clockwise; Angle Clockwise; reserved until the command lands - ("HFQO", Entry::Finish(Action::Pending("home.angleCounterclockwise"))), // pending; Excel: Angle Counterclockwise; Angle Counterclockwise; reserved until the command lands - ("HFS", Entry::Finish(Action::Control("home.fontSize"))), // Excel: Font Size; opens the font-size dropdown - ("HH", Entry::Finish(Action::Control("home.fillColor"))), // Excel: Fill Color; opens the fill-color palette - ("HIC", Entry::Finish(Action::Command("home.insertColumns"))), // Excel: Insert Columns; Insert Columns - ("HIE", Entry::Finish(Action::Command("home.insertCells"))), // Excel: Insert Copied Cells; Insert Cells (copied cells TBD) - ("HII", Entry::Finish(Action::Command("home.insertCells"))), // Excel: Insert Cells - ("HIR", Entry::Finish(Action::Command("home.insertRows"))), // Excel: Insert Rows; Insert Rows - ("HIS", Entry::Finish(Action::Command("home.insertSheet"))), // Excel: Insert Sheet - ("HJ", Entry::Continue), // Excel: Cell Styles; group level: - ("HJN", Entry::Finish(Action::Command("home.newCellStyle"))), // Excel: New Cell Style - ("HK", Entry::Finish(Action::Command("home.comma"))), // Excel: Comma Style + ("HFQ", Entry::Finish(Action::Control("home.orientation"))), // Excel: Orientation Menu; group level: containment(Orientation) + ("HFS", Entry::Finish(Action::Control("home.fontSize"))), // Excel: Font Size; opens the font-size dropdown + ("HH", Entry::Finish(Action::Control("home.fillColor"))), // Excel: Fill Color; opens the fill-color palette + ("HIC", Entry::Finish(Action::Command("home.insertColumns"))), // Excel: Insert Columns; Insert Columns + ("HIE", Entry::Finish(Action::Dialog("insertCells"))), // Excel: Insert Copied Cells; Insert Cells (copied cells TBD) + ("HII", Entry::Finish(Action::Dialog("insertCells"))), // Excel: Insert Cells + ("HIR", Entry::Finish(Action::Command("home.insertRows"))), // Excel: Insert Rows; Insert Rows + ("HIS", Entry::Finish(Action::Command("home.insertSheet"))), // Excel: Insert Sheet + ("HJ", Entry::Continue), // Excel: Cell Styles; group level: + ("HJN", Entry::Finish(Action::Command("home.newCellStyle"))), // Excel: New Cell Style + ("HK", Entry::Finish(Action::Command("home.comma"))), // Excel: Comma Style ("HL", Entry::Continue), // Excel: Conditional Formatting; group level: containment(Conditional Formatting) ("HLC", Entry::Continue), // Excel: Clear Rules Menu; group level: containment(Clear Rules) ("HLCE", Entry::Finish(Action::Command("home.clearRules"))), // Excel: Clear Rules From Sheet; containment(Clear Rules) @@ -130,7 +128,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HOH", Entry::Finish(Action::Dialog("rowHeight"))), // Excel: Row Height ("HOL", Entry::Finish(Action::Command("home.lockCell"))), // Excel: Lock Cell; containment(Lock Cell) ("HOM", Entry::Finish(Action::Dialog("moveSheet"))), // Excel: Move or Copy Sheet - ("HOP", Entry::Finish(Action::Command("review.protectSheet"))), // Excel: Protect Sheet + ("HOP", Entry::Finish(Action::Dialog("protectSheet"))), // Excel: Protect Sheet ("HOR", Entry::Finish(Action::Dialog("renameSheet"))), // Excel: Rename Sheet ("HOT", Entry::Finish(Action::Command("sheet.tabColor"))), // Excel: Tab Color ("HOUC", Entry::Finish(Action::Command("home.hideColumns"))), // Excel: Hide Columns @@ -159,33 +157,33 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HW", Entry::Continue), // Excel: Wrap Text Menu; group level: containment(Wrap Text) ("HWW", Entry::Finish(Action::Command("home.wrapText"))), // Excel: Wrap Text ("HX", Entry::Finish(Action::Command("edit.cut"))), // Excel: Cut - ("NC1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Column Chart; Column chart (kind TBD) + ("NC1", Entry::Finish(Action::Control("insert.chart.column"))), // Excel: Column Chart; Column chart (kind TBD) ("NC2", Entry::Finish(Action::Command("review.newComment"))), // Excel: New Comment - ("NCM", Entry::Finish(Action::Command("review.newComment"))), // Excel: Excel: New Comment (modern) + ("NCM", Entry::Finish(Action::Dialog("comment"))), // Excel: Excel: New Comment (modern) ("NCB", Entry::Finish(Action::Command("insert.checkbox"))), // Excel: Checkbox - ("ND", Entry::Finish(Action::Command("insert.chart"))), // Excel: Scatter Chart; Scatter (kind TBD) + ("ND", Entry::Finish(Action::Control("insert.chart.scatter"))), // Excel: Scatter Chart; Scatter (kind TBD) ("NE", Entry::Finish(Action::Pending("insert.equation"))), // pending; Excel: Equations; Equations; reserved until the command lands ("NF", Entry::Continue), // Excel: Online Pictures; group level: containment(Picture) ("NFP", Entry::Continue), // Excel: From Picture Menu; group level: containment(Picture) ("NFPF", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture From File; containment(Picture) - ("NH1", Entry::Finish(Action::Command("pageLayout.headerFooter"))), // Excel: Header & Footer + ("NH1", Entry::Finish(Action::Dialog("headerFooter"))), // Excel: Header & Footer ("NHF", Entry::Finish(Action::Command("pageLayout.headerFooter"))), // Excel: Header & Footer - ("NHI", Entry::Finish(Action::Command("insert.chart"))), // Excel: Hierarchy Chart; Hierarchy (kind TBD) + ("NHI", Entry::Finish(Action::Control("insert.chart.hierarchy"))), // Excel: Hierarchy Chart; Hierarchy (kind TBD) ("NI1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Waterfall Chart; Waterfall (kind TBD) - ("NI2", Entry::Finish(Action::Command("insert.link"))), // Excel: Link + ("NI2", Entry::Finish(Action::Dialog("insertLink"))), // Excel: Link ("NJ", Entry::Finish(Action::Pending("insert.object"))), // pending; Excel: Object; insert.object missing; reserved until the command lands ("NM1T", Entry::Finish(Action::Command("insert.picture"))), // Excel: SmartArt Picture; containment(Picture) ("NM2", Entry::Finish(Action::Command("insert.chart"))), // Excel: Map Chart; Map (kind TBD) - ("NN1", Entry::Finish(Action::Command("insert.chart"))), // Excel: Line Chart; Line chart (kind TBD) + ("NN1", Entry::Finish(Action::Control("insert.chart.line"))), // Excel: Line Chart; Line chart (kind TBD) ("NNS", Entry::Finish(Action::Command("insert.icons"))), // Excel: Icons ("NP", Entry::Continue), // Excel: Picture; group level: ("NPB", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from File; containment(Picture) ("NPF", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place in Cell; Picture in cell (source TBD) ("NPI", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place in Cell Menu; Picture in cell (source TBD) ("NPO", Entry::Finish(Action::Command("insert.picture"))), // Excel: Place over Cells Menu; Picture over cells (source TBD) - ("NQ", Entry::Finish(Action::Command("insert.chart"))), // Excel: Pie Chart; Pie (kind TBD) + ("NQ", Entry::Finish(Action::Control("insert.chart.pie"))), // Excel: Pie Chart; Pie (kind TBD) ("NR", Entry::Finish(Action::Command("insert.recommendedCharts"))), // Excel: Recommended Charts - ("NSA", Entry::Finish(Action::Command("insert.chart"))), // Excel: Statistical Chart; Statistical chart (kind TBD) + ("NSA", Entry::Finish(Action::Control("insert.chart.statistic"))), // Excel: Statistical Chart; Statistical chart (kind TBD) ("NSC", Entry::Finish(Action::Pending("insert.screenshot"))), // pending; Excel: Screenshot Menu; Screenshot; reserved until the command lands ("NSD", Entry::Finish(Action::Command("insert.chart"))), // Excel: Combo Chart; Combo (kind TBD) ("NSF", Entry::Finish(Action::Pending("insert.slicer"))), // pending; Excel: Slicer; insert.slicer missing (gap #8); reserved until the command lands @@ -219,7 +217,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("PPH", Entry::Finish(Action::Command("pageLayout.printHeadings"))), // Excel: Print Headings ("PR", Entry::Continue), // Excel: Print Area; group level: ("PRS", Entry::Finish(Action::Command("pageLayout.printArea"))), // Excel: Set Print Area - ("PSP", Entry::Finish(Action::Command("pageLayout.pageSetup"))), // Excel: Page Setup + ("PSP", Entry::Finish(Action::Dialog("pageSetup"))), // Excel: Page Setup ("PSZ", Entry::Finish(Action::Command("pageLayout.size"))), // Excel: Size ("PTC", Entry::Finish(Action::Command("pageLayout.themeColors"))), // Excel: Theme Colors; containment(Colors) ("PTE", Entry::Finish(Action::Command("pageLayout.themeEffects"))), // Excel: Theme Effects; containment(Effects) @@ -234,55 +232,41 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("MB", Entry::Finish(Action::Command("formulas.calculateNow"))), // Excel: Calculate Now ("MC", Entry::Finish(Action::Command("formulas.createFromSelection"))), // Excel: Create From Selection ("MD", Entry::Finish(Action::Command("formulas.traceDependents"))), // Excel: Trace Dependents - ("ME", Entry::Continue), // Excel: Date & Time Menu; group level: containment(Date & Time) - ("MEF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Date & Time - Insert Function; containment(Insert Function) - ("MF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Insert Function - ("MG", Entry::Continue), // Excel: Math & Trig Menu; group level: containment(Math & Trig) - ("MGF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Math & Trig - Insert Function; containment(Insert Function) + ("ME", Entry::Finish(Action::Control("formulas.dateTime"))), // Excel: Date & Time Menu; group level: containment(Date & Time) + ("MF", Entry::Finish(Action::Dialog("insertFunction"))), // Excel: Insert Function + ("MG", Entry::Finish(Action::Control("formulas.mathTrig"))), // Excel: Math & Trig Menu; group level: containment(Math & Trig) ("MH", Entry::Finish(Action::Command("formulas.showFormulas"))), // Excel: Show Formulas ("MI", Entry::Continue), // Excel: Financial Menu; group level: containment(Financial) - ("MI1", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Excel: Financial; first item of the dropdown - ("MIF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Financial - Insert Function; containment(Insert Function) + ("MI1", Entry::Finish(Action::Control("formulas.financial"))), // Excel: Excel: Financial; first item of the dropdown ("MJ", Entry::Finish(Action::Command("formulas.calculateSheet"))), // Excel: Calculate Sheet ("MK", Entry::Continue), // Excel: Error Checking Menu; group level: containment(Error Checking) - ("MKK", Entry::Finish(Action::Command("formulas.errorChecking"))), // Excel: Error Checking - ("ML", Entry::Continue), // Excel: Logical Menu; group level: containment(Logical) - ("MLF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Logical - Insert Function; containment(Insert Function) - ("MM", Entry::Finish(Action::Command("formulas.defineName"))), // Excel: Define Name - ("MN", Entry::Finish(Action::Command("formulas.nameManager"))), // Excel: Name Manager - ("MO", Entry::Continue), // Excel: Lookup & Reference Menu; group level: containment(Lookup & Reference) - ("MOF", Entry::Finish(Action::Command("formulas.lookupReference"))), // Excel: Lookup & Reference - Insert Function; containment(Lookup & Reference) - ("MP", Entry::Finish(Action::Command("formulas.tracePrecedents"))), // Excel: Trace Precedents - ("MQ", Entry::Continue), // Excel: More Functions Menu; group level: containment(More Functions) - ("MQCF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Cube - Insert Function; containment(Insert Function) - ("MQEF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Engineering - Insert Function; containment(Insert Function) - ("MQIF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Information - Insert Function; containment(Insert Function) - ("MQOF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Compatibility - Insert Function; containment(Insert Function) - ("MQSF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Statistical - Insert Function; containment(Insert Function) - ("MQWF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Web - Insert Function; containment(Insert Function) - ("MR", Entry::Continue), // Excel: Recently Used Menu; group level: containment(Recently Used) - ("MRF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Recently Used - Insert Function; containment(Insert Function) - ("MS", Entry::Continue), // Excel: Use in Formula Menu; group level: containment(Use in Formula) - ("MSP", Entry::Finish(Action::Pending("formulas.pasteName"))), // pending; Excel: Paste Names; reserved until the command lands - ("MT", Entry::Continue), // Excel: Text Menu; group level: containment(Text) - ("MTF", Entry::Finish(Action::Command("formulas.insertFunction"))), // Excel: Text - Insert Function; containment(Insert Function) - ("MU", Entry::Continue), // Excel: AutoSum Menu; group level: - ("MUA", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Average; AutoSum: Average (variant TBD) - ("MUC", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Count Numbers; AutoSum: Count (variant TBD) - ("MUF", Entry::Finish(Action::Command("formulas.moreFunctions"))), // Excel: More Functions (AutoSum); containment(More Functions) - ("MUI", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Min; AutoSum: Min (variant TBD) - ("MUM", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Max; AutoSum: Max (variant TBD) - ("MUS", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Sum - ("MV", Entry::Finish(Action::Dialog("evaluateFormula"))), // Excel: Evaluate Formula - ("MW", Entry::Finish(Action::Command("formulas.watchWindow"))), // Excel: Watch Window - ("MX", Entry::Continue), // Excel: Calculation Options Menu; group level: containment(Calculation Options) + ("MKK", Entry::Finish(Action::Dialog("errorChecking"))), // Excel: Error Checking + ("ML", Entry::Finish(Action::Control("formulas.logical"))), // Excel: Logical Menu; group level: containment(Logical) + ("MM", Entry::Finish(Action::Dialog("defineName"))), // Excel: Define Name + ("MN", Entry::Finish(Action::Dialog("nameManager"))), // Excel: Name Manager + ("MO", Entry::Finish(Action::Control("formulas.lookup"))), // Excel: Lookup & Reference Menu; group level: containment(Lookup & Reference) + ("MP", Entry::Finish(Action::Command("formulas.tracePrecedents"))), // Excel: Trace Precedents + ("MQ", Entry::Finish(Action::Control("formulas.moreFunctions"))), // Excel: More Functions Menu; group level: containment(More Functions) + ("MS", Entry::Continue), // Excel: Use in Formula Menu; group level: containment(Use in Formula) + ("MSP", Entry::Finish(Action::Pending("formulas.pasteName"))), // pending; Excel: Paste Names; reserved until the command lands + ("MT", Entry::Finish(Action::Control("formulas.text"))), // Excel: Text Menu; group level: containment(Text) + ("MU", Entry::Continue), // Excel: AutoSum Menu; group level: + ("MUA", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Average; AutoSum: Average (variant TBD) + ("MUC", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Count Numbers; AutoSum: Count (variant TBD) + ("MUF", Entry::Finish(Action::Command("formulas.moreFunctions"))), // Excel: More Functions (AutoSum); containment(More Functions) + ("MUI", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Min; AutoSum: Min (variant TBD) + ("MUM", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Max; AutoSum: Max (variant TBD) + ("MUS", Entry::Finish(Action::Command("formulas.autoSum"))), // Excel: Sum + ("MV", Entry::Finish(Action::Dialog("evaluateFormula"))), // Excel: Evaluate Formula + ("MW", Entry::Finish(Action::Command("formulas.watchWindow"))), // Excel: Watch Window + ("MX", Entry::Continue), // Excel: Calculation Options Menu; group level: containment(Calculation Options) ("MXA", Entry::Finish(Action::Command("formulas.calculationOptions"))), // Excel: Automatic Calculation; Calculation options: Automatic ("MXM", Entry::Finish(Action::Command("formulas.calculationOptions"))), // Excel: Manual Calculation; Calculation options: Manual ("MXP", Entry::Finish(Action::Command("formulas.calculationOptions"))), // Excel: Partial Calculation; Calculation options: Partial - ("AB", Entry::Finish(Action::Command("data.subtotal"))), // Excel: Subtotal + ("AB", Entry::Finish(Action::Dialog("subtotal"))), // Excel: Subtotal ("AC", Entry::Finish(Action::Command("data.clearFilter"))), // Excel: Clear Filter ("AD", Entry::Finish(Action::Command("data.consolidate"))), // Excel: Consolidate - ("AE", Entry::Finish(Action::Command("data.textToColumns"))), // Excel: Text to Columns + ("AE", Entry::Finish(Action::Dialog("textToColumns"))), // Excel: Text to Columns ("AF1", Entry::Continue), // Excel: Data from Picture Menu; group level: feature gap; reserved until the command lands ("AF1C", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from Clipboard (source TBD) ("AF1P", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from File (source TBD) @@ -299,7 +283,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("AH", Entry::Finish(Action::Command("data.hideDetail"))), // Excel: Hide Detail ("AJ", Entry::Finish(Action::Command("data.showDetail"))), // Excel: Show Detail ("AK", Entry::Finish(Action::Command("data.queriesConnections"))), // Excel: Edit Links; Edit Links (Queries & Connections); - ("AM", Entry::Finish(Action::Command("data.removeDuplicates"))), // Excel: Remove Duplicates + ("AM", Entry::Finish(Action::Dialog("removeDuplicates"))), // Excel: Remove Duplicates ("AO", Entry::Finish(Action::Command("data.queriesConnections"))), // Excel: Queries & Connections ("AP1", Entry::Finish(Action::Command("file.properties"))), // Excel: Properties ("APN", Entry::Continue), // Excel: Get Data (Power Query) Menu; group level: Power Query variants = GAP @@ -322,7 +306,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("ARS", Entry::Finish(Action::Pending("data.refreshStatus"))), // pending; Excel: Refresh Status; gap #12; reserved until the command lands ("ASA", Entry::Finish(Action::Command("data.sortAscending"))), // Excel: Sort A to Z ("ASD", Entry::Finish(Action::Command("data.sortDescending"))), // Excel: Sort Z to A - ("ASS", Entry::Finish(Action::Command("data.sort"))), // Excel: Sort + ("ASS", Entry::Finish(Action::Dialog("sort"))), // Excel: Sort ("AT", Entry::Finish(Action::Command("data.filter"))), // Excel: Filter ("AU", Entry::Continue), // Excel: Ungroup Menu; group level: containment(Ungroup) ("AUC", Entry::Finish(Action::Pending("data.clearOutline"))), // pending; Excel: Clear Outline; feature gap; reserved until the command lands @@ -337,18 +321,18 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("AWT", Entry::Finish(Action::Command("data.dataTable"))), // Excel: Data Table ("AX", Entry::Finish(Action::Pending("data.existingConnections"))), // pending; Excel: Existing Connections; gap #12; reserved until the command lands ("AY", Entry::Finish(Action::Command("data.reapply"))), // Excel: Reapply Filter; containment(Reapply) - ("RA1", Entry::Finish(Action::Command("review.checkAccessibility"))), // Excel: Check Accessibility + ("RA1", Entry::Finish(Action::Dialog("accessibility"))), // Excel: Check Accessibility ("RA2", Entry::Finish(Action::Pending("review.showComments"))), // pending; Excel: Show All Comments; Show All Comments; reserved until the command lands - ("RC", Entry::Finish(Action::Command("review.newComment"))), // Excel: New Comment + ("RC", Entry::Finish(Action::Dialog("comment"))), // Excel: New Comment ("RD", Entry::Finish(Action::Command("review.deleteComment"))), // Excel: Delete Comment ("RG", Entry::Finish(Action::Pending("review.showChanges"))), // pending; Excel: Track Changes; reserved until the command lands ("RL", Entry::Finish(Action::Pending("review.translate"))), // pending; Excel: Translate; feature gap; reserved until the command lands ("RN", Entry::Finish(Action::Pending("review.nextComment"))), // pending; Excel: Next Comment; Next Comment; reserved until the command lands ("RO", Entry::Finish(Action::Command("review.protectWorkbook"))), // Excel: Protect and Share Workbook - ("RPS", Entry::Finish(Action::Command("review.protectSheet"))), // Excel: Protect Sheet + ("RPS", Entry::Finish(Action::Dialog("protectSheet"))), // Excel: Protect Sheet ("RPW", Entry::Finish(Action::Command("review.protectWorkbook"))), // Excel: Protect Workbook ("RRS", Entry::Finish(Action::Pending("review.smartLookup"))), // pending; Excel: Smart Lookup; feature gap; reserved until the command lands - ("RS", Entry::Finish(Action::Command("review.spelling"))), // Excel: Spelling + ("RS", Entry::Finish(Action::Dialog("spelling"))), // Excel: Spelling ("RTN", Entry::Finish(Action::Command("review.newNote"))), // Excel: New Note ("RV", Entry::Finish(Action::Pending("review.previousComment"))), // pending; Excel: Previous Comment; Previous Comment; reserved until the command lands ("RW", Entry::Finish(Action::Pending("review.shareWorkbook"))), // pending; Excel: Share Workbook; reserved until the command lands diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index 18b6327..ab77e1c 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -389,8 +389,8 @@ }, { "seq": "HDD", - "kind": "command", - "target": "home.deleteCells", + "kind": "dialog", + "target": "deleteCells", "excel": "Delete Cells", "note": "" }, @@ -502,8 +502,8 @@ }, { "seq": "HFDS", - "kind": "command", - "target": "edit.goToSpecial", + "kind": "dialog", + "target": "goToSpecial", "excel": "Go To Special", "note": "" }, @@ -572,8 +572,8 @@ }, { "seq": "HFIS", - "kind": "command", - "target": "edit.fillSeries", + "kind": "dialog", + "target": "series", "excel": "Fill Series", "note": "containment(Series)" }, @@ -607,25 +607,10 @@ }, { "seq": "HFQ", - "kind": "continue", + "kind": "control", "excel": "Orientation Menu", - "note": "group level: containment(Orientation)" - }, - { - "seq": "HFQL", - "kind": "command", - "target": "home.angleClockwise", - "pending": true, - "excel": "Angle Clockwise", - "note": "Angle Clockwise; reserved until the command lands" - }, - { - "seq": "HFQO", - "kind": "command", - "target": "home.angleCounterclockwise", - "pending": true, - "excel": "Angle Counterclockwise", - "note": "Angle Counterclockwise; reserved until the command lands" + "note": "group level: containment(Orientation)", + "target": "home.orientation" }, { "seq": "HFS", @@ -650,15 +635,15 @@ }, { "seq": "HIE", - "kind": "command", - "target": "home.insertCells", + "kind": "dialog", + "target": "insertCells", "excel": "Insert Copied Cells", "note": "Insert Cells (copied cells TBD)" }, { "seq": "HII", - "kind": "command", - "target": "home.insertCells", + "kind": "dialog", + "target": "insertCells", "excel": "Insert Cells", "note": "" }, @@ -849,8 +834,8 @@ }, { "seq": "HOP", - "kind": "command", - "target": "review.protectSheet", + "kind": "dialog", + "target": "protectSheet", "excel": "Protect Sheet", "note": "" }, @@ -1047,8 +1032,8 @@ }, { "seq": "NC1", - "kind": "command", - "target": "insert.chart", + "kind": "control", + "target": "insert.chart.column", "excel": "Column Chart", "note": "Column chart (kind TBD)" }, @@ -1061,8 +1046,8 @@ }, { "seq": "NCM", - "kind": "command", - "target": "review.newComment", + "kind": "dialog", + "target": "comment", "excel": "Excel: New Comment (modern)" }, { @@ -1074,8 +1059,8 @@ }, { "seq": "ND", - "kind": "command", - "target": "insert.chart", + "kind": "control", + "target": "insert.chart.scatter", "excel": "Scatter Chart", "note": "Scatter (kind TBD)" }, @@ -1108,8 +1093,8 @@ }, { "seq": "NH1", - "kind": "command", - "target": "pageLayout.headerFooter", + "kind": "dialog", + "target": "headerFooter", "excel": "Header & Footer", "note": "" }, @@ -1122,8 +1107,8 @@ }, { "seq": "NHI", - "kind": "command", - "target": "insert.chart", + "kind": "control", + "target": "insert.chart.hierarchy", "excel": "Hierarchy Chart", "note": "Hierarchy (kind TBD)" }, @@ -1136,8 +1121,8 @@ }, { "seq": "NI2", - "kind": "command", - "target": "insert.link", + "kind": "dialog", + "target": "insertLink", "excel": "Link", "note": "" }, @@ -1165,8 +1150,8 @@ }, { "seq": "NN1", - "kind": "command", - "target": "insert.chart", + "kind": "control", + "target": "insert.chart.line", "excel": "Line Chart", "note": "Line chart (kind TBD)" }, @@ -1213,8 +1198,8 @@ }, { "seq": "NQ", - "kind": "command", - "target": "insert.chart", + "kind": "control", + "target": "insert.chart.pie", "excel": "Pie Chart", "note": "Pie (kind TBD)" }, @@ -1227,8 +1212,8 @@ }, { "seq": "NSA", - "kind": "command", - "target": "insert.chart", + "kind": "control", + "target": "insert.chart.statistic", "excel": "Statistical Chart", "note": "Statistical chart (kind TBD)" }, @@ -1464,8 +1449,8 @@ }, { "seq": "PSP", - "kind": "command", - "target": "pageLayout.pageSetup", + "kind": "dialog", + "target": "pageSetup", "excel": "Page Setup", "note": "" }, @@ -1568,36 +1553,24 @@ }, { "seq": "ME", - "kind": "continue", + "kind": "control", "excel": "Date & Time Menu", - "note": "group level: containment(Date & Time)" - }, - { - "seq": "MEF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Date & Time - Insert Function", - "note": "containment(Insert Function)" + "note": "group level: containment(Date & Time)", + "target": "formulas.dateTime" }, { "seq": "MF", - "kind": "command", - "target": "formulas.insertFunction", + "kind": "dialog", + "target": "insertFunction", "excel": "Insert Function", "note": "" }, { "seq": "MG", - "kind": "continue", + "kind": "control", "excel": "Math & Trig Menu", - "note": "group level: containment(Math & Trig)" - }, - { - "seq": "MGF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Math & Trig - Insert Function", - "note": "containment(Insert Function)" + "note": "group level: containment(Math & Trig)", + "target": "formulas.mathTrig" }, { "seq": "MH", @@ -1614,17 +1587,10 @@ }, { "seq": "MI1", - "kind": "command", - "target": "formulas.insertFunction", + "kind": "control", + "target": "formulas.financial", "excel": "Excel: Financial; first item of the dropdown" }, - { - "seq": "MIF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Financial - Insert Function", - "note": "containment(Insert Function)" - }, { "seq": "MJ", "kind": "command", @@ -1640,50 +1606,38 @@ }, { "seq": "MKK", - "kind": "command", - "target": "formulas.errorChecking", + "kind": "dialog", + "target": "errorChecking", "excel": "Error Checking", "note": "" }, { "seq": "ML", - "kind": "continue", + "kind": "control", "excel": "Logical Menu", - "note": "group level: containment(Logical)" - }, - { - "seq": "MLF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Logical - Insert Function", - "note": "containment(Insert Function)" + "note": "group level: containment(Logical)", + "target": "formulas.logical" }, { "seq": "MM", - "kind": "command", - "target": "formulas.defineName", + "kind": "dialog", + "target": "defineName", "excel": "Define Name", "note": "" }, { "seq": "MN", - "kind": "command", - "target": "formulas.nameManager", + "kind": "dialog", + "target": "nameManager", "excel": "Name Manager", "note": "" }, { "seq": "MO", - "kind": "continue", + "kind": "control", "excel": "Lookup & Reference Menu", - "note": "group level: containment(Lookup & Reference)" - }, - { - "seq": "MOF", - "kind": "command", - "target": "formulas.lookupReference", - "excel": "Lookup & Reference - Insert Function", - "note": "containment(Lookup & Reference)" + "note": "group level: containment(Lookup & Reference)", + "target": "formulas.lookup" }, { "seq": "MP", @@ -1694,64 +1648,10 @@ }, { "seq": "MQ", - "kind": "continue", + "kind": "control", "excel": "More Functions Menu", - "note": "group level: containment(More Functions)" - }, - { - "seq": "MQCF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Cube - Insert Function", - "note": "containment(Insert Function)" - }, - { - "seq": "MQEF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Engineering - Insert Function", - "note": "containment(Insert Function)" - }, - { - "seq": "MQIF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Information - Insert Function", - "note": "containment(Insert Function)" - }, - { - "seq": "MQOF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Compatibility - Insert Function", - "note": "containment(Insert Function)" - }, - { - "seq": "MQSF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Statistical - Insert Function", - "note": "containment(Insert Function)" - }, - { - "seq": "MQWF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Web - Insert Function", - "note": "containment(Insert Function)" - }, - { - "seq": "MR", - "kind": "continue", - "excel": "Recently Used Menu", - "note": "group level: containment(Recently Used)" - }, - { - "seq": "MRF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Recently Used - Insert Function", - "note": "containment(Insert Function)" + "note": "group level: containment(More Functions)", + "target": "formulas.moreFunctions" }, { "seq": "MS", @@ -1769,16 +1669,10 @@ }, { "seq": "MT", - "kind": "continue", + "kind": "control", "excel": "Text Menu", - "note": "group level: containment(Text)" - }, - { - "seq": "MTF", - "kind": "command", - "target": "formulas.insertFunction", - "excel": "Text - Insert Function", - "note": "containment(Insert Function)" + "note": "group level: containment(Text)", + "target": "formulas.text" }, { "seq": "MU", @@ -1871,8 +1765,8 @@ }, { "seq": "AB", - "kind": "command", - "target": "data.subtotal", + "kind": "dialog", + "target": "subtotal", "excel": "Subtotal", "note": "" }, @@ -1892,8 +1786,8 @@ }, { "seq": "AE", - "kind": "command", - "target": "data.textToColumns", + "kind": "dialog", + "target": "textToColumns", "excel": "Text to Columns", "note": "" }, @@ -2014,8 +1908,8 @@ }, { "seq": "AM", - "kind": "command", - "target": "data.removeDuplicates", + "kind": "dialog", + "target": "removeDuplicates", "excel": "Remove Duplicates", "note": "" }, @@ -2183,8 +2077,8 @@ }, { "seq": "ASS", - "kind": "command", - "target": "data.sort", + "kind": "dialog", + "target": "sort", "excel": "Sort", "note": "" }, @@ -2287,8 +2181,8 @@ }, { "seq": "RA1", - "kind": "command", - "target": "review.checkAccessibility", + "kind": "dialog", + "target": "accessibility", "excel": "Check Accessibility", "note": "" }, @@ -2302,8 +2196,8 @@ }, { "seq": "RC", - "kind": "command", - "target": "review.newComment", + "kind": "dialog", + "target": "comment", "excel": "New Comment", "note": "" }, @@ -2346,8 +2240,8 @@ }, { "seq": "RPS", - "kind": "command", - "target": "review.protectSheet", + "kind": "dialog", + "target": "protectSheet", "excel": "Protect Sheet", "note": "" }, @@ -2368,8 +2262,8 @@ }, { "seq": "RS", - "kind": "command", - "target": "review.spelling", + "kind": "dialog", + "target": "spelling", "excel": "Spelling", "note": "" }, diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 83e448b..5ca18f8 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -358,6 +358,101 @@ fn popup_command(forced: bool, clicked: bool) -> Option { if forced { Some(egui::SetOpenCommand::Bool(true)) } else { clicked.then_some(egui::SetOpenCommand::Toggle) } } +/// Per-control state of an [`editable_combo`]: the filter text and highlighted row. +#[derive(Clone, Default)] +struct ComboState { + text: String, + sel: i32, + was_open: bool, +} + +/// What the user did with an [`editable_combo`]. +enum ComboResult { + None, + /// A row was applied: clicked or Enter on the highlighted row. + Picked(String), + /// Enter with free text that matches no row (e.g. a custom font size). + Entered(String), +} + +/// Excel-style dropdown: a filter field (type to narrow the list) plus arrow/Enter selection. +/// `items` are `(value, display)`; the value is what comes back. When `forced` (keytip invoke) +/// or clicked, the popup opens, focuses the filter field, and selects its text. +fn editable_combo( + ui: &mut Ui, + anchor: &egui::Response, + salt: &'static str, + current_value: &str, + items: &[(&str, String)], + forced: bool, +) -> ComboResult { + let popup_id = anchor.id.with("popup"); + let state_id = anchor.id.with(salt).with("__combo_state"); + let mut st = ui.data_mut(|d| d.get_temp::(state_id)).unwrap_or_default(); + let open_now = egui::Popup::is_id_open(ui.ctx(), popup_id); + if open_now && !st.was_open { + st.text = String::new(); + st.sel = items.iter().position(|(v, _)| *v == current_value).map(|i| i as i32).unwrap_or(0); + } + st.was_open = open_now; + let mut result = ComboResult::None; + egui::Popup::menu(anchor).open_memory(popup_command(forced, anchor.clicked())).show(|ui| { + let text_id = anchor.id.with(salt).with("__combo_text"); + let te = ui.add(egui::TextEdit::singleline(&mut st.text).id(text_id).desired_width(220.0).hint_text("Type to filter…")); + if open_now && !te.has_focus() { + ui.ctx().memory_mut(|m| m.request_focus(text_id)); + // Select all once focus lands so typing replaces the filter. + if let Some(mut s) = egui::text_edit::TextEditState::load(ui.ctx(), text_id) { + let len = st.text.chars().count(); + s.cursor.set_char_range(Some(egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(len)))); + s.store(ui.ctx(), text_id); + } + } + let lower = st.text.to_lowercase(); + let filtered: Vec = items.iter().enumerate().filter(|(_, (_, d))| d.to_lowercase().contains(&lower)).map(|(i, _)| i).collect(); + ui.input(|i| { + if !filtered.is_empty() { + let n = filtered.len() as i32; + if i.key_pressed(egui::Key::ArrowDown) { + st.sel = (st.sel + 1).rem_euclid(n); + } + if i.key_pressed(egui::Key::ArrowUp) { + st.sel = (st.sel - 1).rem_euclid(n); + } + } + if i.key_pressed(egui::Key::Enter) { + if filtered.is_empty() { + let t = st.text.trim().to_string(); + if !t.is_empty() { + result = ComboResult::Entered(t); + } + } else { + let idx = filtered[st.sel.clamp(0, filtered.len() as i32 - 1) as usize]; + result = ComboResult::Picked(items[idx].0.to_string()); + } + } + }); + ui.separator(); + egui::ScrollArea::vertical().max_height(240.0).show(ui, |ui| { + for i in &filtered { + let (value, disp) = &items[*i]; + let row = ui.add(egui::Button::selectable(*i as i32 == st.sel, egui::RichText::new(disp)).min_size(vec2(220.0, 20.0))); + if row.clicked() { + result = ComboResult::Picked(value.to_string()); + } + if row.hovered() { + st.sel = *i as i32; + } + } + }); + if !matches!(result, ComboResult::None) { + ui.close(); + } + }); + ui.data_mut(|d| d.insert_temp(state_id, st)); + result +} + fn menu_items(app: &mut SheetApp, ui: &mut Ui, items: &[(&str, &str, serde_json::Value)]) { for (label, id, p) in items { if *label == "-" { @@ -594,10 +689,9 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { } } let o = small_button(ui, Icon::Orientation, "", "Orientation", true); - for sequence in keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["home.orientation", "dialog:formatCells"]) { - keytip(app, ui, sequence, o.rect.center_bottom()); - } - egui::Popup::menu(&o).show(|ui| { + control_keytip(app, ui, &o, "home.orientation"); + let forced_o = invoke(app, "home.orientation"); + egui::Popup::menu(&o).open_memory(popup_command(forced_o, o.clicked())).show(|ui| { menu_items( app, ui, @@ -944,19 +1038,13 @@ fn home(app: &mut SheetApp, ui: &mut Ui) { } fn font_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { - let mut name = st.font.name.clone(); - let before = name.clone(); + let name = st.font.name.clone(); let resp = ui.add_sized([150.0, 24.0], egui::Button::new(egui::RichText::new(&name).font(theme::ui_font(12.5)))); control_keytip(app, ui, &resp, "home.fontName"); let forced = invoke(app, "home.fontName"); - egui::Popup::menu(&resp).open_memory(popup_command(forced, resp.clicked())).show(|ui| { - for f in FONTS { - let fam = theme::cell_family(f, false, false); - ui.selectable_value(&mut name, f.to_string(), egui::RichText::new(*f).font(egui::FontId::new(14.0, fam))); - } - }); - if name != before { - act(app, "home.fontName", json!({"name": name})); + let items: Vec<(&str, String)> = FONTS.iter().map(|f| (*f, f.to_string())).collect(); + if let ComboResult::Picked(value) = editable_combo(ui, &resp, "font", &name, &items, forced) { + act(app, "home.fontName", json!({"name": value})); } } @@ -981,19 +1069,24 @@ pub const FONTS: &[&str] = &[ ]; fn size_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { - let mut size = st.font.size; - let before = size; + let size = st.font.size; let label = if size.fract() == 0.0 { format!("{}", size as i32) } else { format!("{size}") }; - let resp = ui.add_sized([52.0, 24.0], egui::Button::new(egui::RichText::new(label).font(theme::ui_font(12.5)))); + let resp = ui.add_sized([52.0, 24.0], egui::Button::new(egui::RichText::new(&label).font(theme::ui_font(12.5)))); control_keytip(app, ui, &resp, "home.fontSize"); let forced = invoke(app, "home.fontSize"); - egui::Popup::menu(&resp).open_memory(popup_command(forced, resp.clicked())).show(|ui| { - for s in [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0, 48.0, 72.0] { - ui.selectable_value(&mut size, s, format!("{}", s as i32)); + let sizes = [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 36.0, 48.0, 72.0]; + let labels: Vec = sizes.iter().map(|s| format!("{}", *s as i32)).collect(); + let items: Vec<(&str, String)> = labels.iter().map(|s| (s.as_str(), s.clone())).collect(); + let current = if size.fract() == 0.0 { format!("{}", size as i32) } else { format!("{size}") }; + let mut apply = |s: &str| { + if let Ok(v) = s.parse::() { + act(app, "home.fontSize", json!({"size": v})); } - }); - if size != before { - act(app, "home.fontSize", json!({"size": size})); + }; + match editable_combo(ui, &resp, "size", ¤t, &items, forced) { + ComboResult::Picked(value) => apply(&value), + ComboResult::Entered(text) => apply(&text), + _ => {} } } @@ -1003,29 +1096,24 @@ fn number_combo(app: &mut SheetApp, ui: &mut Ui, st: &Style) { let current = format!("{kind:?}"); let lang = app.ui.language; let sample = app.session.active().and_then(|d| d.wb.active().map(|sh| sh.value(d.selection.active))).unwrap_or_default(); - let mut picked: Option<&str> = None; let resp = ui.add_sized([150.0, 24.0], egui::Button::new(egui::RichText::new(lang.tr(¤t)).font(theme::ui_font(12.5)))); control_keytip(app, ui, &resp, "home.numberFormat"); let forced = invoke(app, "home.numberFormat"); - egui::Popup::menu(&resp).open_memory(popup_command(forced, resp.clicked())).show(|ui| { - for name in ["General", "Number", "Currency", "Accounting", "Short Date", "Long Date", "Time", "Percentage", "Fraction", "Scientific", "Text"] - { - let code = gridcraft_engine::cmd::format::format_code_for(name); + let names = ["General", "Number", "Currency", "Accounting", "Short Date", "Long Date", "Time", "Percentage", "Fraction", "Scientific", "Text"]; + let items: Vec<(&str, String)> = names + .iter() + .map(|n| { + let code = gridcraft_engine::cmd::format::format_code_for(n); let preview = app.session.active().map(|d| gridcraft_engine::display::format(&sample, code, &d.wb).text).unwrap_or_default(); - let r = ui.add(egui::Button::selectable(current == name, format!("{:<12} {preview}", lang.tr(name))).min_size(vec2(240.0, 22.0))); - if r.clicked() { - picked = Some(name); - } + (*n, format!("{:<12} {preview}", lang.tr(n))) + }) + .chain(std::iter::once(("__more", format!("{:>12}", lang.tr("More Number Formats…"))))) + .collect(); + if let ComboResult::Picked(value) = editable_combo(ui, &resp, "number", ¤t, &items, forced) { + match value.as_str() { + "__more" => app.open_dialog("formatCells", json!({"tab": "Number"})), + n => act(app, "home.numberFormat", json!({"format": n})), } - ui.separator(); - if ui.button(lang.tr("More Number Formats…")).clicked() { - picked = Some("__more"); - } - }); - match picked { - Some("__more") => app.open_dialog("formatCells", json!({"tab": "Number"})), - Some(n) => act(app, "home.numberFormat", json!({"format": n})), - None => {} } } @@ -1326,11 +1414,11 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { } ui.vertical(|ui| { ui.horizontal(|ui| { - for (icon, tip, kind, subs) in [ + for (icon, tip, cid, subs) in [ ( Icon::ChartBar, "Column or Bar", - "column", + "insert.chart.column", vec![ ("Clustered Column", "column", ""), ("Stacked Column", "column", "stacked"), @@ -1342,7 +1430,7 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { ( Icon::ChartLine, "Line or Area", - "line", + "insert.chart.line", vec![ ("Line", "line", ""), ("Line with Markers", "line", "markers"), @@ -1351,11 +1439,12 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { ("Stacked Area", "area", "stacked"), ], ), - (Icon::ChartPie, "Pie or Doughnut", "pie", vec![("Pie", "pie", ""), ("Doughnut", "doughnut", "")]), + (Icon::ChartPie, "Pie or Doughnut", "insert.chart.pie", vec![("Pie", "pie", ""), ("Doughnut", "doughnut", "")]), ] { let r = small_button(ui, icon, "", tip, true); - let _ = kind; - egui::Popup::menu(&r).show(|ui| { + control_keytip(app, ui, &r, cid); + let forced_c = invoke(app, cid); + egui::Popup::menu(&r).open_memory(popup_command(forced_c, r.clicked())).show(|ui| { for (label, k, s) in &subs { if ui.button(*label).clicked() { act(app, "insert.chart", json!({"type": k, "subtype": s})); @@ -1365,16 +1454,18 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { } }); ui.horizontal(|ui| { - for (icon, tip, subs) in [ + for (icon, tip, cid, subs) in [ ( Icon::ChartScatter, "Scatter or Bubble", + "insert.chart.scatter", vec![("Scatter", "scatter", ""), ("Scatter with Lines", "scatter", "lines"), ("Bubble", "bubble", "")], ), - (Icon::Chart, "Statistic", vec![("Histogram", "histogram", ""), ("Box & Whisker", "boxWhisker", "")]), + (Icon::Chart, "Statistic", "insert.chart.statistic", vec![("Histogram", "histogram", ""), ("Box & Whisker", "boxWhisker", "")]), ( Icon::Theme, "Hierarchy & more", + "insert.chart.hierarchy", vec![ ("Treemap", "treemap", ""), ("Sunburst", "sunburst", ""), @@ -1387,7 +1478,9 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { ), ] { let r = small_button(ui, icon, "", tip, true); - egui::Popup::menu(&r).show(|ui| { + control_keytip(app, ui, &r, cid); + let forced_c = invoke(app, cid); + egui::Popup::menu(&r).open_memory(popup_command(forced_c, r.clicked())).show(|ui| { for (label, k, s) in &subs { if ui.button(*label).clicked() { act(app, "insert.chart", json!({"type": k, "subtype": s})); @@ -1403,7 +1496,7 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { } sep(ui); let sp = big_button(ui, Icon::Sparkline, "Sparklines", "Sparklines", true); - anchor_keytip(app, ui, &sp, &["insert.sparkline"]); + anchor_keytip(app, ui, &sp, &["dialog:sparkline"]); egui::Popup::menu(&sp).show(|ui| { for (label, kind) in [("Line", "line"), ("Column", "column"), ("Win/Loss", "winLoss")] { if ui.button(label).clicked() { @@ -1418,12 +1511,12 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { } sep(ui); let __k = big_button(ui, Icon::Link, "Link", "Insert Link (⌘K)", false); - control_keytip(app, ui, &__k, "insertLink"); + control_keytip(app, ui, &__k, "dialog:insertLink"); if __k.clicked() { app.open_dialog("insertLink", json!({})); } let __k = big_button(ui, Icon::Comment, "Comment", "New Comment", false); - control_keytip(app, ui, &__k, "comment"); + control_keytip(app, ui, &__k, "dialog:comment"); if __k.clicked() { app.open_dialog("comment", json!({"threaded": true})); } @@ -1434,7 +1527,7 @@ fn insert(app: &mut SheetApp, ui: &mut Ui) { act(app, "insert.textBox", json!({})); } let __k = big_button(ui, Icon::PageLayout, "Header &\nFooter", "Header & Footer", false); - control_keytip(app, ui, &__k, "headerFooter"); + control_keytip(app, ui, &__k, "dialog:headerFooter"); if __k.clicked() { // As in Excel: Page Layout view, editing the header in place (the dialog is under Page Setup). crate::paper_view::start_editing(app); @@ -1618,7 +1711,7 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { fn formulas(app: &mut SheetApp, ui: &mut Ui) { let __k = big_button(ui, Icon::Function, "Insert\nFunction", "Insert Function (⇧F3)", false); - control_keytip(app, ui, &__k, "insertFunction"); + control_keytip(app, ui, &__k, "dialog:insertFunction"); if __k.clicked() { app.open_dialog("insertFunction", json!({})); } @@ -1627,17 +1720,19 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { if s.clicked() { act(app, "formulas.autoSum", json!({})); } - for (label, cat) in [ - ("Financial", "Financial"), - ("Logical", "Logical"), - ("Text", "Text"), - ("Date &\nTime", "DateTime"), - ("Lookup &\nReference", "Lookup"), - ("Math &\nTrig", "MathTrig"), - ("More\nFunctions", "Statistical"), + for (label, cat, cid) in [ + ("Financial", "Financial", "formulas.financial"), + ("Logical", "Logical", "formulas.logical"), + ("Text", "Text", "formulas.text"), + ("Date &\nTime", "DateTime", "formulas.dateTime"), + ("Lookup &\nReference", "Lookup", "formulas.lookup"), + ("Math &\nTrig", "MathTrig", "formulas.mathTrig"), + ("More\nFunctions", "Statistical", "formulas.moreFunctions"), ] { let r = big_button(ui, Icon::Book, label, label, true); - egui::Popup::menu(&r).show(|ui| { + control_keytip(app, ui, &r, cid); + let forced_c = invoke(app, cid); + egui::Popup::menu(&r).open_memory(popup_command(forced_c, r.clicked())).show(|ui| { ui.set_max_height(420.0); egui::ScrollArea::vertical().show(ui, |ui| { for f in gridcraft_engine::cmd::formulas::function_list() { @@ -1659,18 +1754,18 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { } sep(ui); let __k = big_button(ui, Icon::Name, "Name\nManager", "Name Manager", false); - control_keytip(app, ui, &__k, "nameManager"); + control_keytip(app, ui, &__k, "dialog:nameManager"); if __k.clicked() { app.open_dialog("nameManager", json!({})); } ui.vertical(|ui| { let __k = small_button(ui, Icon::Name, "Define Name", "Define Name", false); - control_keytip(app, ui, &__k, "defineName"); + control_keytip(app, ui, &__k, "dialog:defineName"); if __k.clicked() { app.open_dialog("defineName", json!({})); } let use_in = small_button(ui, Icon::Function, "Use in Formula", "Use in Formula", true); - anchor_keytip(app, ui, &use_in, &["formulas.useInFormula"]); + anchor_keytip(app, ui, &use_in, &["formulas.pasteName"]); egui::Popup::menu(&use_in).show(|ui| { let names: Vec = app.session.active().map(|d| d.wb.names.iter().map(|n| n.name.clone()).collect()).unwrap_or_default(); for n in names { @@ -1709,12 +1804,12 @@ fn formulas(app: &mut SheetApp, ui: &mut Ui) { act(app, "formulas.showFormulas", json!({})); } let __k = small_button(ui, Icon::Validation, "Error Checking", "Error Checking", false); - control_keytip(app, ui, &__k, "errorChecking"); + control_keytip(app, ui, &__k, "dialog:errorChecking"); if __k.clicked() { app.open_dialog("errorChecking", json!({})); } let __k = small_button(ui, Icon::Calc, "Evaluate Formula", "Evaluate Formula", false); - control_keytip(app, ui, &__k, "evaluateFormula"); + control_keytip(app, ui, &__k, "dialog:evaluateFormula"); if __k.clicked() { app.open_dialog("evaluateFormula", json!({})); } @@ -1772,7 +1867,7 @@ fn data(app: &mut SheetApp, ui: &mut Ui) { } }); let __k = big_button(ui, Icon::SortFilter, "Sort", "Custom Sort", false); - control_keytip(app, ui, &__k, "sort"); + control_keytip(app, ui, &__k, "dialog:sort"); if __k.clicked() { app.open_dialog("sort", json!({})); } @@ -1795,7 +1890,7 @@ fn data(app: &mut SheetApp, ui: &mut Ui) { }); sep(ui); let __k = big_button(ui, Icon::TextColumns, "Text to\nColumns", "Text to Columns", false); - control_keytip(app, ui, &__k, "textToColumns"); + control_keytip(app, ui, &__k, "dialog:textToColumns"); if __k.clicked() { app.open_dialog("textToColumns", json!({})); } @@ -1805,14 +1900,12 @@ fn data(app: &mut SheetApp, ui: &mut Ui) { act(app, "edit.flashFill", json!({})); } let __k = big_button(ui, Icon::Duplicates, "Remove\nDuplicates", "Remove Duplicates", false); - control_keytip(app, ui, &__k, "removeDuplicates"); + control_keytip(app, ui, &__k, "dialog:removeDuplicates"); if __k.clicked() { app.open_dialog("removeDuplicates", json!({})); } let dv = big_button(ui, Icon::Validation, "Data\nValidation", "Data Validation", true); - for sequence in - keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["dialog:dataValidation", "data.circleInvalid", "data.validation"]) - { + for sequence in keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["data.validation", "data.circleInvalid"]) { keytip(app, ui, sequence, dv.rect.center_bottom()); } egui::Popup::menu(&dv).show(|ui| { @@ -1860,7 +1953,7 @@ fn data(app: &mut SheetApp, ui: &mut Ui) { act(app, "data.ungroup", json!({})); } let __k = small_button(ui, Icon::Subtotal, "Subtotal", "Subtotal", false); - control_keytip(app, ui, &__k, "subtotal"); + control_keytip(app, ui, &__k, "dialog:subtotal"); if __k.clicked() { app.open_dialog("subtotal", json!({})); } @@ -1885,7 +1978,7 @@ fn data(app: &mut SheetApp, ui: &mut Ui) { fn review(app: &mut SheetApp, ui: &mut Ui) { let __k = big_button(ui, Icon::Spell, "Spelling", "Spelling (F7)", false); - control_keytip(app, ui, &__k, "spelling"); + control_keytip(app, ui, &__k, "dialog:spelling"); if __k.clicked() { app.open_dialog("spelling", json!({})); } @@ -1895,13 +1988,13 @@ fn review(app: &mut SheetApp, ui: &mut Ui) { app.open_dialog("statistics", json!({})); } let __k = big_button(ui, Icon::Validation, "Check\nAccessibility", "Check Accessibility", false); - control_keytip(app, ui, &__k, "accessibility"); + control_keytip(app, ui, &__k, "dialog:accessibility"); if __k.clicked() { app.open_dialog("accessibility", json!({})); } sep(ui); let __k = big_button(ui, Icon::Comment, "New\nComment", "New Comment", false); - control_keytip(app, ui, &__k, "comment"); + control_keytip(app, ui, &__k, "dialog:comment"); if __k.clicked() { app.open_dialog("comment", json!({"threaded": true})); } @@ -1935,7 +2028,7 @@ fn review(app: &mut SheetApp, ui: &mut Ui) { } } let notes = big_button(ui, Icon::Note, "Notes", "Notes", true); - anchor_keytip(app, ui, ¬es, &["review.newNote"]); + anchor_keytip(app, ui, ¬es, &["review.nextComment"]); egui::Popup::menu(¬es).show(|ui| { menu_items( app, @@ -1952,7 +2045,7 @@ fn review(app: &mut SheetApp, ui: &mut Ui) { sep(ui); let protected = app.session.active().and_then(|d| d.wb.active().map(|s| s.is_protected())).unwrap_or(false); let __k = big_button(ui, Icon::Lock, if protected { "Unprotect\nSheet" } else { "Protect\nSheet" }, "Protect Sheet", false); - control_keytip(app, ui, &__k, "review.protectSheet"); + control_keytip(app, ui, &__k, "dialog:protectSheet"); if __k.clicked() { if protected { app.open_dialog("unprotectSheet", json!({})); @@ -2018,7 +2111,7 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { }); sep(ui); let __k = big_button(ui, Icon::Zoom, "Zoom", "Zoom…", false); - control_keytip(app, ui, &__k, "zoom"); + control_keytip(app, ui, &__k, "dialog:zoom"); if __k.clicked() { app.open_dialog("zoom", json!({})); } @@ -2035,10 +2128,9 @@ fn view(app: &mut SheetApp, ui: &mut Ui) { } sep(ui); let fp = big_button(ui, Icon::Freeze, "Freeze\nPanes", "Freeze Panes", true); - for sequence in keytip_table::group_extension( - app.keytips.prefix().unwrap_or_default(), - &["view.freezePanes", "view.freezeTopRow", "view.freezeFirstColumn", "view.unfreezePanes"], - ) { + for sequence in + keytip_table::group_extension(app.keytips.prefix().unwrap_or_default(), &["view.freezePanes", "view.freezeTopRow", "view.freezeFirstColumn"]) + { keytip(app, ui, sequence, fp.rect.center_bottom()); } egui::Popup::menu(&fp).show(|ui| { diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index a045593..0f35966 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -456,6 +456,47 @@ fn control_keytips_open_dropdowns_and_run_handlers() { assert_eq!(h.state().keytips.prefix(), None); } +#[test] +fn editable_combos_filter_by_typing_and_move_by_arrows() { + // Open the font dropdown (HFF), type to filter, then Enter to apply. + let mut h = harness(blank()); + sequence(&mut h, &[Key::H, Key::F, Key::F], false); + h.run_steps(3); // let the filter field gain focus + events(&mut h, [Event::Text("arial".into())]); + h.run_steps(3); + // "arial" matches Arial only — Georgia must be filtered out of the list. + assert_eq!(h.query_all_by_label("Georgia").count(), 0, "typing must narrow the list"); + assert!(h.query_all_by_label("Arial").count() >= 1, "the match must stay visible"); + key(&mut h, Key::Enter); + h.run_steps(3); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "applying must not raise an error"); + // The popup closed and the combo button now shows the applied font. + assert_eq!(h.query_all_by_label("Arial").count(), 1, "Enter must apply Arial and close the popup"); + assert_eq!(h.query_all_by_label("Georgia").count(), 0); + + // Arrows alone move through the full list: Calibri -> Cambria -> Candara -> Consolas. + let mut h = harness(blank()); + sequence(&mut h, &[Key::H, Key::F, Key::F], false); + h.run_steps(3); + for _ in 0..3 { + key(&mut h, Key::ArrowDown); + } + key(&mut h, Key::Enter); + h.run_steps(3); + assert!(h.state().message.is_none() && h.state().toast.is_none()); + assert_eq!(h.query_all_by_label("Consolas").count(), 1, "Down x3 from Calibri must land on Consolas"); + + // Size combo: type a custom size not in the list, Enter applies it. + let mut h = harness(blank()); + sequence(&mut h, &[Key::H, Key::F, Key::S], false); + h.run_steps(3); + events(&mut h, [Event::Text("13.5".into())]); + key(&mut h, Key::Enter); + h.run_steps(3); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "custom sizes must apply"); + assert_eq!(h.query_all_by_label("13.5").count(), 1, "the button must show the custom size"); +} + #[test] fn every_sequence_terminates_without_panicking() { use gridcraft_ui_egui::keytips::keytip_table::SEQUENCES; diff --git a/crates/ui-egui/tests/keytips_generated.rs b/crates/ui-egui/tests/keytips_generated.rs index 6e52dc9..edc0109 100644 --- a/crates/ui-egui/tests/keytips_generated.rs +++ b/crates/ui-egui/tests/keytips_generated.rs @@ -487,7 +487,7 @@ fn keytip_hdc_() { fn keytip_hdd_() { let mut h = harness(blank()); sequence(&mut h, "HDD", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.deleteCells"), "HDD must journal home.deleteCells"); + assert!(h.state().dialog.is_some(), "HDD must open the deleteCells dialog"); } #[test] @@ -603,7 +603,7 @@ fn keytip_hfdr_() { fn keytip_hfds_() { let mut h = harness(blank()); sequence(&mut h, "HFDS", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.goToSpecial"), "HFDS must journal edit.goToSpecial"); + assert!(h.state().dialog.is_some(), "HFDS must open the goToSpecial dialog"); } #[test] @@ -677,7 +677,7 @@ fn keytip_hfir_() { fn keytip_hfis_() { let mut h = harness(blank()); sequence(&mut h, "HFIS", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "edit.fillSeries"), "HFIS must journal edit.fillSeries"); + assert!(h.state().dialog.is_some(), "HFIS must open the series dialog"); } #[test] @@ -712,27 +712,8 @@ fn keytip_hfp_() { fn keytip_hfq_() { let mut h = harness(blank()); sequence(&mut h, "HFQ", false); - assert_eq!(h.state().keytips.prefix(), Some("HFQ"), "HFQ must stay armed waiting for more input"); -} - -#[test] -fn keytip_hfql_() { - let mut h = harness(blank()); - sequence(&mut h, "HFQL", false); - assert!(h.state().dialog.is_none(), "HFQL must not open a dialog"); - assert!(h.state().message.is_none(), "HFQL must not open a message box"); - assert!(h.state().toast.is_none(), "HFQL must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "HFQL must not journal"); -} - -#[test] -fn keytip_hfqo_() { - let mut h = harness(blank()); - sequence(&mut h, "HFQO", false); - assert!(h.state().dialog.is_none(), "HFQO must not open a dialog"); - assert!(h.state().message.is_none(), "HFQO must not open a message box"); - assert!(h.state().toast.is_none(), "HFQO must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "HFQO must not journal"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "HFQ must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -762,14 +743,14 @@ fn keytip_hic_() { fn keytip_hie_() { let mut h = harness(blank()); sequence(&mut h, "HIE", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertCells"), "HIE must journal home.insertCells"); + assert!(h.state().dialog.is_some(), "HIE must open the insertCells dialog"); } #[test] fn keytip_hii_() { let mut h = harness(blank()); sequence(&mut h, "HII", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "home.insertCells"), "HII must journal home.insertCells"); + assert!(h.state().dialog.is_some(), "HII must open the insertCells dialog"); } #[test] @@ -966,7 +947,7 @@ fn keytip_hom_() { fn keytip_hop_() { let mut h = harness(blank()); sequence(&mut h, "HOP", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectSheet"), "HOP must journal review.protectSheet"); + assert!(h.state().dialog.is_some(), "HOP must open the protectSheet dialog"); } #[test] @@ -1169,7 +1150,8 @@ fn keytip_hx_() { fn keytip_nc1_() { let mut h = harness(blank()); sequence(&mut h, "NC1", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NC1 must journal insert.chart"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "NC1 must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1183,7 +1165,7 @@ fn keytip_nc2_() { fn keytip_ncm_() { let mut h = harness(blank()); sequence(&mut h, "NCM", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newComment"), "NCM must journal review.newComment"); + assert!(h.state().dialog.is_some(), "NCM must open the comment dialog"); } #[test] @@ -1197,7 +1179,8 @@ fn keytip_ncb_() { fn keytip_nd_() { let mut h = harness(blank()); sequence(&mut h, "ND", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "ND must journal insert.chart"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "ND must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1235,7 +1218,7 @@ fn keytip_nfpf_() { fn keytip_nh1_() { let mut h = harness(blank()); sequence(&mut h, "NH1", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.headerFooter"), "NH1 must journal pageLayout.headerFooter"); + assert!(h.state().dialog.is_some(), "NH1 must open the headerFooter dialog"); } #[test] @@ -1249,7 +1232,8 @@ fn keytip_nhf_() { fn keytip_nhi_() { let mut h = harness(blank()); sequence(&mut h, "NHI", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NHI must journal insert.chart"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "NHI must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1263,7 +1247,7 @@ fn keytip_ni1_() { fn keytip_ni2_() { let mut h = harness(blank()); sequence(&mut h, "NI2", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.link"), "NI2 must journal insert.link"); + assert!(h.state().dialog.is_some(), "NI2 must open the insertLink dialog"); } #[test] @@ -1294,7 +1278,8 @@ fn keytip_nm2_() { fn keytip_nn1_() { let mut h = harness(blank()); sequence(&mut h, "NN1", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NN1 must journal insert.chart"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "NN1 must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1343,7 +1328,8 @@ fn keytip_npo_() { fn keytip_nq_() { let mut h = harness(blank()); sequence(&mut h, "NQ", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NQ must journal insert.chart"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "NQ must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1357,7 +1343,8 @@ fn keytip_nr_() { fn keytip_nsa_() { let mut h = harness(blank()); sequence(&mut h, "NSA", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.chart"), "NSA must journal insert.chart"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "NSA must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1613,7 +1600,7 @@ fn keytip_prs_() { fn keytip_psp_() { let mut h = harness(blank()); sequence(&mut h, "PSP", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.pageSetup"), "PSP must journal pageLayout.pageSetup"); + assert!(h.state().dialog.is_some(), "PSP must open the pageSetup dialog"); } #[test] @@ -1721,35 +1708,23 @@ fn keytip_md_() { fn keytip_me_() { let mut h = harness(blank()); sequence(&mut h, "ME", false); - assert_eq!(h.state().keytips.prefix(), Some("ME"), "ME must stay armed waiting for more input"); -} - -#[test] -fn keytip_mef_() { - let mut h = harness(blank()); - sequence(&mut h, "MEF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MEF must journal formulas.insertFunction"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "ME must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] fn keytip_mf_() { let mut h = harness(blank()); sequence(&mut h, "MF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MF must journal formulas.insertFunction"); + assert!(h.state().dialog.is_some(), "MF must open the insertFunction dialog"); } #[test] fn keytip_mg_() { let mut h = harness(blank()); sequence(&mut h, "MG", false); - assert_eq!(h.state().keytips.prefix(), Some("MG"), "MG must stay armed waiting for more input"); -} - -#[test] -fn keytip_mgf_() { - let mut h = harness(blank()); - sequence(&mut h, "MGF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MGF must journal formulas.insertFunction"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MG must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1770,14 +1745,8 @@ fn keytip_mi_() { fn keytip_mi1_() { let mut h = harness(blank()); sequence(&mut h, "MI1", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MI1 must journal formulas.insertFunction"); -} - -#[test] -fn keytip_mif_() { - let mut h = harness(blank()); - sequence(&mut h, "MIF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MIF must journal formulas.insertFunction"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MI1 must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1798,49 +1767,37 @@ fn keytip_mk_() { fn keytip_mkk_() { let mut h = harness(blank()); sequence(&mut h, "MKK", false); - assert!(h.state().message.is_none() && h.state().toast.is_none(), "MKK must run formulas.errorChecking cleanly"); + assert!(h.state().dialog.is_some(), "MKK must open the errorChecking dialog"); } #[test] fn keytip_ml_() { let mut h = harness(blank()); sequence(&mut h, "ML", false); - assert_eq!(h.state().keytips.prefix(), Some("ML"), "ML must stay armed waiting for more input"); -} - -#[test] -fn keytip_mlf_() { - let mut h = harness(blank()); - sequence(&mut h, "MLF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MLF must journal formulas.insertFunction"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "ML must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] fn keytip_mm_() { let mut h = harness(blank()); sequence(&mut h, "MM", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.defineName"), "MM must journal formulas.defineName"); + assert!(h.state().dialog.is_some(), "MM must open the defineName dialog"); } #[test] fn keytip_mn_() { let mut h = harness(blank()); sequence(&mut h, "MN", false); - assert!(h.state().message.is_none() && h.state().toast.is_none(), "MN must run formulas.nameManager cleanly"); + assert!(h.state().dialog.is_some(), "MN must open the nameManager dialog"); } #[test] fn keytip_mo_() { let mut h = harness(blank()); sequence(&mut h, "MO", false); - assert_eq!(h.state().keytips.prefix(), Some("MO"), "MO must stay armed waiting for more input"); -} - -#[test] -fn keytip_mof_() { - let mut h = harness(blank()); - sequence(&mut h, "MOF", false); - assert!(h.state().message.is_none() && h.state().toast.is_none(), "MOF must run formulas.lookupReference cleanly"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MO must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1854,63 +1811,8 @@ fn keytip_mp_() { fn keytip_mq_() { let mut h = harness(blank()); sequence(&mut h, "MQ", false); - assert_eq!(h.state().keytips.prefix(), Some("MQ"), "MQ must stay armed waiting for more input"); -} - -#[test] -fn keytip_mqcf_() { - let mut h = harness(blank()); - sequence(&mut h, "MQCF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQCF must journal formulas.insertFunction"); -} - -#[test] -fn keytip_mqef_() { - let mut h = harness(blank()); - sequence(&mut h, "MQEF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQEF must journal formulas.insertFunction"); -} - -#[test] -fn keytip_mqif_() { - let mut h = harness(blank()); - sequence(&mut h, "MQIF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQIF must journal formulas.insertFunction"); -} - -#[test] -fn keytip_mqof_() { - let mut h = harness(blank()); - sequence(&mut h, "MQOF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQOF must journal formulas.insertFunction"); -} - -#[test] -fn keytip_mqsf_() { - let mut h = harness(blank()); - sequence(&mut h, "MQSF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQSF must journal formulas.insertFunction"); -} - -#[test] -fn keytip_mqwf_() { - let mut h = harness(blank()); - sequence(&mut h, "MQWF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MQWF must journal formulas.insertFunction"); -} - -#[test] -fn keytip_mr_() { - let mut h = harness(blank()); - sequence(&mut h, "MR", false); - assert_eq!(h.state().keytips.prefix(), Some("MR"), "MR must stay armed waiting for more input"); -} - -#[test] -fn keytip_mrf_() { - let mut h = harness(blank()); - sequence(&mut h, "MRF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MRF must journal formulas.insertFunction"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MQ must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -1934,14 +1836,8 @@ fn keytip_msp_() { fn keytip_mt_() { let mut h = harness(blank()); sequence(&mut h, "MT", false); - assert_eq!(h.state().keytips.prefix(), Some("MT"), "MT must stay armed waiting for more input"); -} - -#[test] -fn keytip_mtf_() { - let mut h = harness(blank()); - sequence(&mut h, "MTF", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "formulas.insertFunction"), "MTF must journal formulas.insertFunction"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "MT must route to its control without an error box"); + assert_eq!(h.state().keytips.prefix(), None); } #[test] @@ -2039,7 +1935,7 @@ fn keytip_mxp_() { fn keytip_ab_() { let mut h = harness(blank()); sequence(&mut h, "AB", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.subtotal"), "AB must journal data.subtotal"); + assert!(h.state().dialog.is_some(), "AB must open the subtotal dialog"); } #[test] @@ -2060,7 +1956,7 @@ fn keytip_ad_() { fn keytip_ae_() { let mut h = harness(blank()); sequence(&mut h, "AE", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.textToColumns"), "AE must journal data.textToColumns"); + assert!(h.state().dialog.is_some(), "AE must open the textToColumns dialog"); } #[test] @@ -2200,7 +2096,7 @@ fn keytip_ak_() { fn keytip_am_() { let mut h = harness(blank()); sequence(&mut h, "AM", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.removeDuplicates"), "AM must journal data.removeDuplicates"); + assert!(h.state().dialog.is_some(), "AM must open the removeDuplicates dialog"); } #[test] @@ -2394,7 +2290,7 @@ fn keytip_asd_() { fn keytip_ass_() { let mut h = harness(blank()); sequence(&mut h, "ASS", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "data.sort"), "ASS must journal data.sort"); + assert!(h.state().dialog.is_some(), "ASS must open the sort dialog"); } #[test] @@ -2508,7 +2404,7 @@ fn keytip_ay_() { fn keytip_ra1_() { let mut h = harness(blank()); sequence(&mut h, "RA1", false); - assert!(h.state().message.is_none() && h.state().toast.is_none(), "RA1 must run review.checkAccessibility cleanly"); + assert!(h.state().dialog.is_some(), "RA1 must open the accessibility dialog"); } #[test] @@ -2525,7 +2421,7 @@ fn keytip_ra2_() { fn keytip_rc_() { let mut h = harness(blank()); sequence(&mut h, "RC", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.newComment"), "RC must journal review.newComment"); + assert!(h.state().dialog.is_some(), "RC must open the comment dialog"); } #[test] @@ -2576,7 +2472,7 @@ fn keytip_ro_() { fn keytip_rps_() { let mut h = harness(blank()); sequence(&mut h, "RPS", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "review.protectSheet"), "RPS must journal review.protectSheet"); + assert!(h.state().dialog.is_some(), "RPS must open the protectSheet dialog"); } #[test] @@ -2600,7 +2496,7 @@ fn keytip_rrs_() { fn keytip_rs_() { let mut h = harness(blank()); sequence(&mut h, "RS", false); - assert!(h.state().message.is_some() || h.state().toast.is_some(), "RS must invoke review.spelling and surface its missing-params error"); + assert!(h.state().dialog.is_some(), "RS must open the spelling dialog"); } #[test] @@ -3033,7 +2929,7 @@ fn keytip_9_() { #[test] fn generated_test_count_matches_the_table() { - const EXPECTED: usize = 397; + const EXPECTED: usize = 381; assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED); } From d2a691aea879dd0df2ba31e8225ed3cf8397dfaf Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 22:16:14 +0300 Subject: [PATCH 12/15] Fix combo navigation edge cases and wire Page Layout Sheet Options badges - Arrows/Enter while a keytip combo is open now navigate the list instead of moving the filter field's text cursor (events stripped from TextEdit) - Reopening a combo resets the filter: the reset frame requests a repaint so the stale filtered list never lingers on screen - Page Layout Sheet Options: Gridlines View/Print and Headings View/Print checkboxes painted with VG/PG/VH/PH badges and keytip toggles - Background rebound from PG to PGB (Excel's Page Setup GB badge); PG is no longer terminal --- .../src/keytips/keytip_table/generated.rs | 2 +- crates/ui-egui/src/keytips/keytips.json | 5320 ++++++++--------- crates/ui-egui/src/ribbon.rs | 80 +- crates/ui-egui/tests/keytips.rs | 23 + crates/ui-egui/tests/keytips_generated.rs | 6 +- 5 files changed, 2739 insertions(+), 2692 deletions(-) diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index 479e5bc..9a95b01 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -208,7 +208,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("PB", Entry::Continue), // Excel: Breaks; group level: ("PBI", Entry::Finish(Action::Command("pageLayout.breaks"))), // Excel: Insert Page Break; Insert Page Break ("PBR", Entry::Finish(Action::Command("pageLayout.breaks"))), // Excel: Remove Page Break; Remove Page Break - ("PG", Entry::Finish(Action::Command("pageLayout.background"))), // Excel: Background + ("PGB", Entry::Finish(Action::Command("pageLayout.background"))), // Excel: Background; Excel badge GB (Background in the Page Setup group) ("PI", Entry::Finish(Action::Command("pageLayout.printTitles"))), // Excel: Print Titles ("PM", Entry::Continue), // Excel: Margins; group level: ("PMA", Entry::Finish(Action::Command("pageLayout.margins"))), // Excel: Custom Margins; containment(Margins) diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index ab77e1c..dd04f7b 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -1,2662 +1,2662 @@ { - "doc": [ - "Single source of truth for Excel-style sequential access keys (Alt+H+X+X style).", - "The generated keytip_table/generated.rs is produced from this file: `cargo xtask keytips`.", - "Adding or removing a shortcut is a one-line edit here. Rules:", - " - seq: the access-key sequence, letters and digits only (two-letter units like \"JI\" are one entry).", - " - kind: \"tab\" (switches a ribbon tab), \"command\" (runs a command id), \"dialog\" (opens a dialog by name),", - " \"continue\" (an explicit group level with no deeper sequences yet).", - " - Intermediate levels need no entries: the matcher waits automatically when deeper sequences exist.", - " - pending: true = the command does not exist yet; the sequence stays reserved and silent until it lands.", - " - Keep entries grouped by tab and sorted by sequence within a group; the FIRST entry for a tab is its canonical badge (legacy aliases come after it)." - ], - "sequences": [ - { - "seq": "H", - "kind": "tab", - "target": "Home" - }, - { - "seq": "N", - "kind": "tab", - "target": "Insert" - }, - { - "seq": "JI", - "kind": "tab", - "target": "Draw", - "note": "Two-letter unit. Deeper Draw letters differ between Excel and GridCraft; left blank for future contributors." - }, - { - "seq": "P", - "kind": "tab", - "target": "Page Layout" - }, - { - "seq": "M", - "kind": "tab", - "target": "Formulas" - }, - { - "seq": "A", - "kind": "tab", - "target": "Data" - }, - { - "seq": "R", - "kind": "tab", - "target": "Review" - }, - { - "seq": "W", - "kind": "tab", - "target": "View" - }, - { - "seq": "E", - "kind": "tab", - "target": "Home", - "note": "Legacy Edit-menu path (Excel 2003); leads to the same Home menus." - }, - { - "seq": "O", - "kind": "tab", - "target": "Home", - "note": "Legacy Format-menu path (Excel 2003); leads to the same Home menus." - }, - { - "seq": "HAR", - "kind": "command", - "target": "home.alignRight" - }, - { - "seq": "HAT", - "kind": "command", - "target": "home.alignTop", - "excel": "Excel: Top Align" - }, - { - "seq": "HAM", - "kind": "command", - "target": "home.alignMiddle", - "excel": "Excel: Middle Align" - }, - { - "seq": "HAB", - "kind": "command", - "target": "home.alignBottom", - "excel": "Excel: Bottom Align" - }, - { - "seq": "HAL", - "kind": "command", - "target": "home.alignLeft" - }, - { - "seq": "HAC", - "kind": "command", - "target": "home.alignCenter" - }, - { - "seq": "HVS", - "kind": "dialog", - "target": "pasteSpecial" - }, - { - "seq": "ES", - "kind": "dialog", - "target": "pasteSpecial", - "excel": "Classic Paste Special dialog (legacy Edit route); A/F/T/V are dialog mnemonics" - }, - { - "seq": "HOI", - "kind": "command", - "target": "home.autofitColumnWidth" - }, - { - "seq": "OCA", - "kind": "command", - "target": "home.autofitColumnWidth", - "note": "Legacy Format menu." - }, - { - "seq": "WVG", - "kind": "command", - "target": "view.gridlines" - }, - { - "seq": "0", - "kind": "command", - "target": "qat.10", - "pending": true, - "excel": "Quick Access Toolbar position 10", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "1", - "kind": "command", - "target": "qat.1", - "pending": true, - "excel": "Quick Access Toolbar position 1", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "2", - "kind": "command", - "target": "qat.2", - "pending": true, - "excel": "Quick Access Toolbar position 2", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "3", - "kind": "command", - "target": "qat.3", - "pending": true, - "excel": "Quick Access Toolbar position 3", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "4", - "kind": "command", - "target": "qat.4", - "pending": true, - "excel": "Quick Access Toolbar position 4", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "5", - "kind": "command", - "target": "qat.5", - "pending": true, - "excel": "Quick Access Toolbar position 5", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "H0", - "kind": "command", - "target": "home.increaseDecimal", - "excel": "Increase Decimals", - "note": "containment(Increase Decimal)" - }, - { - "seq": "H1", - "kind": "command", - "target": "home.bold", - "excel": "Bold", - "note": "" - }, - { - "seq": "H2", - "kind": "command", - "target": "home.italic", - "excel": "Italic", - "note": "" - }, - { - "seq": "H3", - "kind": "command", - "excel": "Excel: Underline; fires immediately (not a menu)", - "target": "home.underline" - }, - { - "seq": "H5", - "kind": "command", - "target": "home.decreaseIndent", - "excel": "Decrease Indent", - "note": "" - }, - { - "seq": "H6", - "kind": "command", - "target": "home.increaseIndent", - "excel": "Increase Indent", - "note": "" - }, - { - "seq": "H9", - "kind": "command", - "target": "home.decreaseDecimal", - "excel": "Decrease Decimals", - "note": "containment(Decrease Decimal)" - }, - { - "seq": "HAN", - "kind": "command", - "excel": "Accounting Format", - "note": "fires immediately in Excel; deeper aliases dropped as unreachable", - "target": "home.accounting" - }, - { - "seq": "HAS", - "kind": "command", - "target": "home.addIns", - "excel": "Add-ins", - "note": "containment(Add-ins)" - }, - { - "seq": "HB", - "kind": "continue", - "excel": "Borders Menu", - "note": "group level:" - }, - { - "seq": "HBA", - "kind": "command", - "target": "home.borders", - "excel": "All Borders", - "note": "containment(Borders)" - }, - { - "seq": "HBB", - "kind": "command", - "target": "home.borders", - "excel": "Bottom Double Border", - "note": "border-side TBD" - }, - { - "seq": "HBC", - "kind": "command", - "target": "home.borders", - "excel": "Top and Thick Bottom Border", - "note": "border-side TBD" - }, - { - "seq": "HBD", - "kind": "command", - "target": "home.borders", - "excel": "Top and Bottom Border", - "note": "border-side TBD" - }, - { - "seq": "HBE", - "kind": "command", - "target": "home.borders", - "excel": "Erase Border", - "note": "border-side TBD" - }, - { - "seq": "HBG", - "kind": "command", - "target": "home.drawBorder", - "excel": "Draw Border Grid", - "note": "containment(Draw Border)" - }, - { - "seq": "HBH", - "kind": "command", - "target": "home.borders", - "excel": "Thick Bottom Border", - "note": "border-side TBD" - }, - { - "seq": "HBI", - "kind": "command", - "target": "home.borders", - "excel": "Line Color", - "note": "Line Color (submenu TBD)" - }, - { - "seq": "HBL", - "kind": "command", - "target": "home.borders", - "excel": "Left Border", - "note": "border-side TBD" - }, - { - "seq": "HBM", - "kind": "command", - "target": "home.borders", - "excel": "More Borders", - "note": "containment(Borders)" - }, - { - "seq": "HBN", - "kind": "command", - "target": "home.borders", - "excel": "No Border", - "note": "border-side TBD" - }, - { - "seq": "HBO", - "kind": "command", - "target": "home.borders", - "excel": "Bottom Border", - "note": "border-side TBD" - }, - { - "seq": "HBP", - "kind": "command", - "target": "home.borders", - "excel": "Top Border", - "note": "border-side TBD" - }, - { - "seq": "HBR", - "kind": "command", - "target": "home.borders", - "excel": "Right Border", - "note": "border-side TBD" - }, - { - "seq": "HBS", - "kind": "command", - "target": "home.borders", - "excel": "Outside Borders", - "note": "containment(Borders)" - }, - { - "seq": "HBT", - "kind": "command", - "target": "home.borders", - "excel": "Thick Outside Borders", - "note": "border-side TBD" - }, - { - "seq": "HBU", - "kind": "command", - "target": "home.borders", - "excel": "Top and Double Bottom Border", - "note": "border-side TBD" - }, - { - "seq": "HBW", - "kind": "command", - "target": "home.drawBorder", - "excel": "Draw Border", - "note": "" - }, - { - "seq": "HBY", - "kind": "command", - "target": "home.borders", - "excel": "Line Style", - "note": "Line Style (submenu TBD)" - }, - { - "seq": "HC", - "kind": "command", - "excel": "Copy", - "note": "fires immediately in Excel; deeper aliases dropped as unreachable", - "target": "edit.copy" - }, - { - "seq": "HDC", - "kind": "command", - "target": "home.deleteColumns", - "excel": "Delete Columns", - "note": "Delete Columns" - }, - { - "seq": "HDD", - "kind": "dialog", - "target": "deleteCells", - "excel": "Delete Cells", - "note": "" - }, - { - "seq": "HDR", - "kind": "command", - "target": "home.deleteRows", - "excel": "Delete Rows", - "note": "Delete Rows" - }, - { - "seq": "HDS", - "kind": "command", - "target": "home.deleteSheet", - "excel": "Delete Sheet", - "note": "" - }, - { - "seq": "HEA", - "kind": "command", - "target": "edit.clearAll", - "excel": "Clear All", - "note": "" - }, - { - "seq": "HEC", - "kind": "command", - "target": "edit.clearContents", - "excel": "Clear Contents", - "note": "" - }, - { - "seq": "HEF", - "kind": "command", - "target": "edit.clearFormats", - "excel": "Clear Formats", - "note": "" - }, - { - "seq": "HEL", - "kind": "command", - "target": "edit.clearHyperlinks", - "excel": "Clear Hyperlinks", - "note": "" - }, - { - "seq": "HEM", - "kind": "command", - "target": "edit.clearComments", - "excel": "Clear Comments and Notes", - "note": "containment(Clear Comments and Notes)" - }, - { - "seq": "HFC", - "kind": "control", - "target": "home.fontColor", - "excel": "Font Color", - "note": "opens the font-color palette" - }, - { - "seq": "HFDC", - "kind": "command", - "target": "home.conditionalFormat", - "excel": "Find Conditional Formatting", - "note": "containment(Conditional Formatting)" - }, - { - "seq": "HFDF", - "kind": "dialog", - "target": "find", - "excel": "Find", - "note": "" - }, - { - "seq": "HFDG", - "kind": "dialog", - "target": "goTo", - "excel": "Go To", - "note": "" - }, - { - "seq": "HFDM", - "kind": "command", - "target": "edit.find", - "excel": "Find Notes", - "note": "containment(Find)" - }, - { - "seq": "HFDO", - "kind": "command", - "target": "home.selectObjects", - "pending": true, - "excel": "Select Objects", - "note": "Select Objects; reserved until the command lands" - }, - { - "seq": "HFDP", - "kind": "command", - "target": "arrange.selectionPane", - "excel": "Selection Pane", - "note": "" - }, - { - "seq": "HFDR", - "kind": "dialog", - "target": "find", - "excel": "Replace", - "note": "opens Find and Replace" - }, - { - "seq": "HFDS", - "kind": "dialog", - "target": "goToSpecial", - "excel": "Go To Special", - "note": "" - }, - { - "seq": "HFF", - "kind": "control", - "target": "home.fontName", - "excel": "Font Selection", - "note": "opens the font dropdown" - }, - { - "seq": "HFG", - "kind": "command", - "target": "home.increaseFontSize", - "excel": "Increase Font Size", - "note": "" - }, - { - "seq": "HFI", - "kind": "continue", - "excel": "Fill", - "note": "group level: containment(Fill Up)" - }, - { - "seq": "HFIA", - "kind": "command", - "target": "home.fillAcrossWorksheets", - "pending": true, - "excel": "Fill Across Worksheets", - "note": "Fill Across Worksheets; reserved until the command lands" - }, - { - "seq": "HFID", - "kind": "command", - "target": "edit.fillDown", - "excel": "Fill Down", - "note": "" - }, - { - "seq": "HFIF", - "kind": "command", - "target": "edit.flashFill", - "excel": "Flash Fill", - "note": "" - }, - { - "seq": "HFIJ", - "kind": "command", - "target": "home.justify", - "excel": "Fill Justify", - "note": "containment(Justify)" - }, - { - "seq": "HFIL", - "kind": "command", - "target": "edit.fillLeft", - "excel": "Fill Left", - "note": "" - }, - { - "seq": "HFIR", - "kind": "command", - "target": "edit.fillRight", - "excel": "Fill Right", - "note": "" - }, - { - "seq": "HFIS", - "kind": "dialog", - "target": "series", - "excel": "Fill Series", - "note": "containment(Series)" - }, - { - "seq": "HFIU", - "kind": "command", - "target": "edit.fillUp", - "excel": "Fill Up", - "note": "" - }, - { - "seq": "HFK", - "kind": "command", - "target": "home.decreaseFontSize", - "excel": "Decrease Font Size", - "note": "" - }, - { - "seq": "HFM", - "kind": "dialog", - "target": "formatCells", - "excel": "More Number Formats", - "note": "More Number Formats opens Format Cells" - }, - { - "seq": "HFP", - "kind": "command", - "target": "edit.formatPainter", - "excel": "Format Painter", - "note": "" - }, - { - "seq": "HFQ", - "kind": "control", - "excel": "Orientation Menu", - "note": "group level: containment(Orientation)", - "target": "home.orientation" - }, - { - "seq": "HFS", - "kind": "control", - "target": "home.fontSize", - "excel": "Font Size", - "note": "opens the font-size dropdown" - }, - { - "seq": "HH", - "kind": "control", - "target": "home.fillColor", - "excel": "Fill Color", - "note": "opens the fill-color palette" - }, - { - "seq": "HIC", - "kind": "command", - "target": "home.insertColumns", - "excel": "Insert Columns", - "note": "Insert Columns" - }, - { - "seq": "HIE", - "kind": "dialog", - "target": "insertCells", - "excel": "Insert Copied Cells", - "note": "Insert Cells (copied cells TBD)" - }, - { - "seq": "HII", - "kind": "dialog", - "target": "insertCells", - "excel": "Insert Cells", - "note": "" - }, - { - "seq": "HIR", - "kind": "command", - "target": "home.insertRows", - "excel": "Insert Rows", - "note": "Insert Rows" - }, - { - "seq": "HIS", - "kind": "command", - "target": "home.insertSheet", - "excel": "Insert Sheet", - "note": "" - }, - { - "seq": "HJ", - "kind": "continue", - "excel": "Cell Styles", - "note": "group level:" - }, - { - "seq": "HJN", - "kind": "command", - "target": "home.newCellStyle", - "excel": "New Cell Style", - "note": "" - }, - { - "seq": "HK", - "kind": "command", - "target": "home.comma", - "excel": "Comma Style", - "note": "" - }, - { - "seq": "HL", - "kind": "continue", - "excel": "Conditional Formatting", - "note": "group level: containment(Conditional Formatting)" - }, - { - "seq": "HLC", - "kind": "continue", - "excel": "Clear Rules Menu", - "note": "group level: containment(Clear Rules)" - }, - { - "seq": "HLCE", - "kind": "command", - "target": "home.clearRules", - "excel": "Clear Rules From Sheet", - "note": "containment(Clear Rules)" - }, - { - "seq": "HLCS", - "kind": "command", - "target": "home.clearRules", - "excel": "Clear Rules From Cells", - "note": "containment(Clear Rules)" - }, - { - "seq": "HLCT", - "kind": "command", - "target": "home.clearRules", - "excel": "Clear Rules From Table", - "note": "containment(Clear Rules)" - }, - { - "seq": "HLD", - "kind": "command", - "target": "home.conditionalFormat", - "excel": "Data Bars", - "note": "Data Bars (submenu TBD)" - }, - { - "seq": "HLH", - "kind": "command", - "target": "home.conditionalFormat", - "excel": "Highlight Cells", - "note": "Highlight Cells (submenu TBD)" - }, - { - "seq": "HLN", - "kind": "command", - "target": "home.conditionalFormat", - "excel": "New Rule", - "note": "New Rule (submenu TBD)" - }, - { - "seq": "HLR", - "kind": "command", - "target": "home.manageRules", - "excel": "Manage Rules", - "note": "containment(Manage Rules)" - }, - { - "seq": "HLT", - "kind": "command", - "target": "home.conditionalFormat", - "excel": "Top/Bottom Rules", - "note": "Top/Bottom (submenu TBD)" - }, - { - "seq": "HM", - "kind": "continue", - "excel": "Merge & Center Menu", - "note": "group level: containment(Merge & Center)" - }, - { - "seq": "HMA", - "kind": "command", - "target": "home.mergeAcross", - "excel": "Merge Across", - "note": "" - }, - { - "seq": "HMC", - "kind": "command", - "target": "home.mergeCenter", - "excel": "Merge & Center", - "note": "" - }, - { - "seq": "HMM", - "kind": "command", - "target": "home.mergeCells", - "excel": "Merge Cells", - "note": "" - }, - { - "seq": "HMU", - "kind": "command", - "target": "home.unmergeCells", - "excel": "Unmerge Cells", - "note": "" - }, - { - "seq": "HN", - "kind": "control", - "target": "home.numberFormat", - "excel": "Number Format", - "note": "opens the number-format dropdown" - }, - { - "seq": "HOA", - "kind": "command", - "target": "home.autofitRowHeight", - "excel": "AutoFit Row Height", - "note": "" - }, - { - "seq": "HOD", - "kind": "dialog", - "target": "defaultWidth", - "excel": "Default Column Width", - "note": "containment(Column Width)" - }, - { - "seq": "HOE", - "kind": "command", - "target": "home.formatCells", - "excel": "Format Cells", - "note": "" - }, - { - "seq": "HOH", - "kind": "dialog", - "target": "rowHeight", - "excel": "Row Height", - "note": "" - }, - { - "seq": "HOL", - "kind": "command", - "target": "home.lockCell", - "excel": "Lock Cell", - "note": "containment(Lock Cell)" - }, - { - "seq": "HOM", - "kind": "dialog", - "target": "moveSheet", - "excel": "Move or Copy Sheet", - "note": "" - }, - { - "seq": "HOP", - "kind": "dialog", - "target": "protectSheet", - "excel": "Protect Sheet", - "note": "" - }, - { - "seq": "HOR", - "kind": "dialog", - "target": "renameSheet", - "excel": "Rename Sheet", - "note": "" - }, - { - "seq": "HOT", - "kind": "command", - "target": "sheet.tabColor", - "excel": "Tab Color", - "note": "" - }, - { - "seq": "HOUC", - "kind": "command", - "target": "home.hideColumns", - "excel": "Hide Columns", - "note": "" - }, - { - "seq": "HOUH", - "kind": "command", - "target": "sheet.unhide", - "excel": "Unhide Sheet", - "note": "" - }, - { - "seq": "HOUL", - "kind": "command", - "target": "home.unhideColumns", - "excel": "Unhide Columns", - "note": "" - }, - { - "seq": "HOUO", - "kind": "command", - "target": "home.unhideRows", - "excel": "Unhide Rows", - "note": "" - }, - { - "seq": "HOUR", - "kind": "command", - "target": "home.hideRows", - "excel": "Hide Rows", - "note": "" - }, - { - "seq": "HOUS", - "kind": "command", - "target": "sheet.hide", - "excel": "Hide Sheet", - "note": "" - }, - { - "seq": "HOW", - "kind": "dialog", - "target": "columnWidth", - "excel": "Column Width", - "note": "" - }, - { - "seq": "HP", - "kind": "command", - "target": "home.percent", - "excel": "Excel: Percentage Style" - }, - { - "seq": "HSC", - "kind": "command", - "target": "data.clearFilter", - "excel": "Clear Filter", - "note": "" - }, - { - "seq": "HSF", - "kind": "command", - "target": "data.filter", - "excel": "Filter", - "note": "" - }, - { - "seq": "HSO", - "kind": "command", - "target": "data.sortDescending", - "excel": "Sort Z to A", - "note": "" - }, - { - "seq": "HSS", - "kind": "command", - "target": "data.sortAscending", - "excel": "Sort A to Z", - "note": "" - }, - { - "seq": "HSU", - "kind": "command", - "target": "data.sort", - "excel": "Custom Sort", - "note": "" - }, - { - "seq": "HT", - "kind": "continue", - "excel": "Format as Table", - "note": "group level:" - }, - { - "seq": "HTN", - "kind": "command", - "target": "table.style", - "excel": "New Table Style", - "note": "" - }, - { - "seq": "HTP", - "kind": "command", - "target": "table.style", - "excel": "New Pivot Table Style", - "note": "" - }, - { - "seq": "HU", - "kind": "continue", - "excel": "AutoSum", - "note": "group level:" - }, - { - "seq": "HUA", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Auto Average", - "note": "AutoSum: Average (variant TBD)" - }, - { - "seq": "HUC", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Auto Count Numbers", - "note": "AutoSum: Count (variant TBD)" - }, - { - "seq": "HUI", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Auto Min", - "note": "AutoSum: Min (variant TBD)" - }, - { - "seq": "HUM", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Auto Max", - "note": "AutoSum: Max (variant TBD)" - }, - { - "seq": "HV", - "kind": "continue", - "excel": "Paste", - "note": "group level:" - }, - { - "seq": "HVP", - "kind": "command", - "target": "edit.pasteSpecial", - "excel": "Paste", - "note": "" - }, - { - "seq": "HW", - "kind": "continue", - "excel": "Wrap Text Menu", - "note": "group level: containment(Wrap Text)" - }, - { - "seq": "HWW", - "kind": "command", - "target": "home.wrapText", - "excel": "Wrap Text", - "note": "" - }, - { - "seq": "HX", - "kind": "command", - "target": "edit.cut", - "excel": "Cut", - "note": "" - }, - { - "seq": "NC1", - "kind": "control", - "target": "insert.chart.column", - "excel": "Column Chart", - "note": "Column chart (kind TBD)" - }, - { - "seq": "NC2", - "kind": "command", - "target": "review.newComment", - "excel": "New Comment", - "note": "" - }, - { - "seq": "NCM", - "kind": "dialog", - "target": "comment", - "excel": "Excel: New Comment (modern)" - }, - { - "seq": "NCB", - "kind": "command", - "target": "insert.checkbox", - "excel": "Checkbox", - "note": "" - }, - { - "seq": "ND", - "kind": "control", - "target": "insert.chart.scatter", - "excel": "Scatter Chart", - "note": "Scatter (kind TBD)" - }, - { - "seq": "NE", - "kind": "command", - "target": "insert.equation", - "pending": true, - "excel": "Equations", - "note": "Equations; reserved until the command lands" - }, - { - "seq": "NF", - "kind": "continue", - "excel": "Online Pictures", - "note": "group level: containment(Picture)" - }, - { - "seq": "NFP", - "kind": "continue", - "excel": "From Picture Menu", - "note": "group level: containment(Picture)" - }, - { - "seq": "NFPF", - "kind": "command", - "target": "insert.picture", - "excel": "Picture From File", - "note": "containment(Picture)" - }, - { - "seq": "NH1", - "kind": "dialog", - "target": "headerFooter", - "excel": "Header & Footer", - "note": "" - }, - { - "seq": "NHF", - "kind": "command", - "target": "pageLayout.headerFooter", - "excel": "Header & Footer", - "note": "" - }, - { - "seq": "NHI", - "kind": "control", - "target": "insert.chart.hierarchy", - "excel": "Hierarchy Chart", - "note": "Hierarchy (kind TBD)" - }, - { - "seq": "NI1", - "kind": "command", - "target": "insert.chart", - "excel": "Waterfall Chart", - "note": "Waterfall (kind TBD)" - }, - { - "seq": "NI2", - "kind": "dialog", - "target": "insertLink", - "excel": "Link", - "note": "" - }, - { - "seq": "NJ", - "kind": "command", - "target": "insert.object", - "pending": true, - "excel": "Object", - "note": "insert.object missing; reserved until the command lands" - }, - { - "seq": "NM1T", - "kind": "command", - "target": "insert.picture", - "excel": "SmartArt Picture", - "note": "containment(Picture)" - }, - { - "seq": "NM2", - "kind": "command", - "target": "insert.chart", - "excel": "Map Chart", - "note": "Map (kind TBD)" - }, - { - "seq": "NN1", - "kind": "control", - "target": "insert.chart.line", - "excel": "Line Chart", - "note": "Line chart (kind TBD)" - }, - { - "seq": "NNS", - "kind": "command", - "target": "insert.icons", - "excel": "Icons", - "note": "" - }, - { - "seq": "NP", - "kind": "continue", - "excel": "Picture", - "note": "group level:" - }, - { - "seq": "NPB", - "kind": "command", - "target": "insert.picture", - "excel": "Picture from File", - "note": "containment(Picture)" - }, - { - "seq": "NPF", - "kind": "command", - "target": "insert.picture", - "excel": "Place in Cell", - "note": "Picture in cell (source TBD)" - }, - { - "seq": "NPI", - "kind": "command", - "target": "insert.picture", - "excel": "Place in Cell Menu", - "note": "Picture in cell (source TBD)" - }, - { - "seq": "NPO", - "kind": "command", - "target": "insert.picture", - "excel": "Place over Cells Menu", - "note": "Picture over cells (source TBD)" - }, - { - "seq": "NQ", - "kind": "control", - "target": "insert.chart.pie", - "excel": "Pie Chart", - "note": "Pie (kind TBD)" - }, - { - "seq": "NR", - "kind": "command", - "target": "insert.recommendedCharts", - "excel": "Recommended Charts", - "note": "" - }, - { - "seq": "NSA", - "kind": "control", - "target": "insert.chart.statistic", - "excel": "Statistical Chart", - "note": "Statistical chart (kind TBD)" - }, - { - "seq": "NSC", - "kind": "command", - "target": "insert.screenshot", - "pending": true, - "excel": "Screenshot Menu", - "note": "Screenshot; reserved until the command lands" - }, - { - "seq": "NSD", - "kind": "command", - "target": "insert.chart", - "excel": "Combo Chart", - "note": "Combo (kind TBD)" - }, - { - "seq": "NSF", - "kind": "command", - "target": "insert.slicer", - "pending": true, - "excel": "Slicer", - "note": "insert.slicer missing (gap #8); reserved until the command lands" - }, - { - "seq": "NSH", - "kind": "command", - "target": "insert.shape", - "excel": "Shapes", - "note": "" - }, - { - "seq": "NSP", - "kind": "command", - "target": "insert.recommendedPivotTables", - "excel": "Recommended PivotTables", - "note": "" - }, - { - "seq": "NST", - "kind": "command", - "target": "insert.timeline", - "pending": true, - "excel": "Timeline", - "note": "insert.timeline missing (gap #8); reserved until the command lands" - }, - { - "seq": "NSZ", - "kind": "command", - "target": "insert.pivotChart", - "pending": true, - "excel": "PivotChart", - "note": "PivotChart; reserved until the command lands" - }, - { - "seq": "NT", - "kind": "command", - "target": "insert.table", - "excel": "Table", - "note": "" - }, - { - "seq": "NU", - "kind": "command", - "target": "insert.symbol", - "excel": "Symbols", - "note": "containment(Symbol)" - }, - { - "seq": "NZS", - "kind": "command", - "target": "insert.symbol", - "excel": "Excel: Symbol (modern)" - }, - { - "seq": "NV", - "kind": "command", - "target": "insert.pivotTable", - "excel": "PivotTable", - "note": "" - }, - { - "seq": "NW", - "kind": "command", - "target": "insert.wordArt", - "pending": true, - "excel": "WordArt", - "note": "WordArt; reserved until the command lands" - }, - { - "seq": "NX", - "kind": "command", - "target": "insert.textBox", - "excel": "Text Box", - "note": "" - }, - { - "seq": "NZK", - "kind": "continue", - "excel": "Sparklines Menu", - "note": "group level:" - }, - { - "seq": "NZKSL", - "kind": "dialog", - "target": "sparkline", - "excel": "Line Sparkline", - "note": "" - }, - { - "seq": "NZKSO", - "kind": "dialog", - "target": "sparkline", - "excel": "Column Sparkline", - "note": "" - }, - { - "seq": "NZKSW", - "kind": "dialog", - "target": "sparkline", - "excel": "Win/Loss Sparkline", - "note": "" - }, - { - "seq": "NZTX", - "kind": "command", - "target": "insert.textBox", - "excel": "Text Box", - "note": "" - }, - { - "seq": "NZT", - "kind": "command", - "target": "insert.textBox", - "excel": "Excel: Text Box (modern)" - }, - { - "seq": "PAE", - "kind": "command", - "target": "arrange.sendBackward", - "excel": "Send Backward", - "note": "" - }, - { - "seq": "PAF", - "kind": "command", - "target": "arrange.bringForward", - "excel": "Bring Forward", - "note": "" - }, - { - "seq": "PB", - "kind": "continue", - "excel": "Breaks", - "note": "group level:" - }, - { - "seq": "PBI", - "kind": "command", - "target": "pageLayout.breaks", - "excel": "Insert Page Break", - "note": "Insert Page Break" - }, - { - "seq": "PBR", - "kind": "command", - "target": "pageLayout.breaks", - "excel": "Remove Page Break", - "note": "Remove Page Break" - }, - { - "seq": "PG", - "kind": "command", - "target": "pageLayout.background", - "excel": "Background", - "note": "" - }, - { - "seq": "PI", - "kind": "command", - "target": "pageLayout.printTitles", - "excel": "Print Titles", - "note": "" - }, - { - "seq": "PM", - "kind": "continue", - "excel": "Margins", - "note": "group level:" - }, - { - "seq": "PMA", - "kind": "command", - "target": "pageLayout.margins", - "excel": "Custom Margins", - "note": "containment(Margins)" - }, - { - "seq": "PO", - "kind": "command", - "target": "pageLayout.orientation", - "excel": "Orientation", - "note": "" - }, - { - "seq": "PPG", - "kind": "command", - "target": "pageLayout.printGridlines", - "excel": "Print Gridlines", - "note": "" - }, - { - "seq": "PPH", - "kind": "command", - "target": "pageLayout.printHeadings", - "excel": "Print Headings", - "note": "" - }, - { - "seq": "PR", - "kind": "continue", - "excel": "Print Area", - "note": "group level:" - }, - { - "seq": "PRS", - "kind": "command", - "target": "pageLayout.printArea", - "excel": "Set Print Area", - "note": "" - }, - { - "seq": "PSP", - "kind": "dialog", - "target": "pageSetup", - "excel": "Page Setup", - "note": "" - }, - { - "seq": "PSZ", - "kind": "command", - "target": "pageLayout.size", - "excel": "Size", - "note": "" - }, - { - "seq": "PTC", - "kind": "command", - "target": "pageLayout.themeColors", - "excel": "Theme Colors", - "note": "containment(Colors)" - }, - { - "seq": "PTE", - "kind": "command", - "target": "pageLayout.themeEffects", - "excel": "Theme Effects", - "note": "containment(Effects)" - }, - { - "seq": "PTF", - "kind": "command", - "target": "pageLayout.themeFonts", - "excel": "Theme Fonts", - "note": "containment(Fonts)" - }, - { - "seq": "PTH", - "kind": "continue", - "excel": "Themes", - "note": "group level:" - }, - { - "seq": "PTHB", - "kind": "command", - "target": "pageLayout.theme", - "excel": "Browse Themes", - "note": "containment(Themes)" - }, - { - "seq": "PVG", - "kind": "command", - "target": "view.gridlines", - "excel": "View Gridlines", - "note": "" - }, - { - "seq": "PVH", - "kind": "command", - "target": "view.headings", - "excel": "View Headings", - "note": "" - }, - { - "seq": "MA", - "kind": "continue", - "excel": "Remove Arrows Menu", - "note": "group level: containment(Remove Arrows)" - }, - { - "seq": "MAA", - "kind": "command", - "target": "formulas.removeArrows", - "excel": "Remove All Arrows", - "note": "landed in the catalog; live since T7 probe" - }, - { - "seq": "MAP", - "kind": "command", - "target": "formulas.removePrecedentArrows", - "pending": true, - "excel": "Remove Precedent Arrows", - "note": "Remove Precedent Arrows; reserved until the command lands" - }, - { - "seq": "MB", - "kind": "command", - "target": "formulas.calculateNow", - "excel": "Calculate Now", - "note": "" - }, - { - "seq": "MC", - "kind": "command", - "target": "formulas.createFromSelection", - "excel": "Create From Selection", - "note": "" - }, - { - "seq": "MD", - "kind": "command", - "target": "formulas.traceDependents", - "excel": "Trace Dependents", - "note": "" - }, - { - "seq": "ME", - "kind": "control", - "excel": "Date & Time Menu", - "note": "group level: containment(Date & Time)", - "target": "formulas.dateTime" - }, - { - "seq": "MF", - "kind": "dialog", - "target": "insertFunction", - "excel": "Insert Function", - "note": "" - }, - { - "seq": "MG", - "kind": "control", - "excel": "Math & Trig Menu", - "note": "group level: containment(Math & Trig)", - "target": "formulas.mathTrig" - }, - { - "seq": "MH", - "kind": "command", - "target": "formulas.showFormulas", - "excel": "Show Formulas", - "note": "" - }, - { - "seq": "MI", - "kind": "continue", - "excel": "Financial Menu", - "note": "group level: containment(Financial)" - }, - { - "seq": "MI1", - "kind": "control", - "target": "formulas.financial", - "excel": "Excel: Financial; first item of the dropdown" - }, - { - "seq": "MJ", - "kind": "command", - "target": "formulas.calculateSheet", - "excel": "Calculate Sheet", - "note": "" - }, - { - "seq": "MK", - "kind": "continue", - "excel": "Error Checking Menu", - "note": "group level: containment(Error Checking)" - }, - { - "seq": "MKK", - "kind": "dialog", - "target": "errorChecking", - "excel": "Error Checking", - "note": "" - }, - { - "seq": "ML", - "kind": "control", - "excel": "Logical Menu", - "note": "group level: containment(Logical)", - "target": "formulas.logical" - }, - { - "seq": "MM", - "kind": "dialog", - "target": "defineName", - "excel": "Define Name", - "note": "" - }, - { - "seq": "MN", - "kind": "dialog", - "target": "nameManager", - "excel": "Name Manager", - "note": "" - }, - { - "seq": "MO", - "kind": "control", - "excel": "Lookup & Reference Menu", - "note": "group level: containment(Lookup & Reference)", - "target": "formulas.lookup" - }, - { - "seq": "MP", - "kind": "command", - "target": "formulas.tracePrecedents", - "excel": "Trace Precedents", - "note": "" - }, - { - "seq": "MQ", - "kind": "control", - "excel": "More Functions Menu", - "note": "group level: containment(More Functions)", - "target": "formulas.moreFunctions" - }, - { - "seq": "MS", - "kind": "continue", - "excel": "Use in Formula Menu", - "note": "group level: containment(Use in Formula)" - }, - { - "seq": "MSP", - "kind": "command", - "target": "formulas.pasteName", - "excel": "Paste Names", - "note": "reserved until the command lands", - "pending": true - }, - { - "seq": "MT", - "kind": "control", - "excel": "Text Menu", - "note": "group level: containment(Text)", - "target": "formulas.text" - }, - { - "seq": "MU", - "kind": "continue", - "excel": "AutoSum Menu", - "note": "group level:" - }, - { - "seq": "MUA", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Average", - "note": "AutoSum: Average (variant TBD)" - }, - { - "seq": "MUC", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Count Numbers", - "note": "AutoSum: Count (variant TBD)" - }, - { - "seq": "MUF", - "kind": "command", - "target": "formulas.moreFunctions", - "excel": "More Functions (AutoSum)", - "note": "containment(More Functions)" - }, - { - "seq": "MUI", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Min", - "note": "AutoSum: Min (variant TBD)" - }, - { - "seq": "MUM", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Max", - "note": "AutoSum: Max (variant TBD)" - }, - { - "seq": "MUS", - "kind": "command", - "target": "formulas.autoSum", - "excel": "Sum", - "note": "" - }, - { - "seq": "MV", - "kind": "dialog", - "target": "evaluateFormula", - "excel": "Evaluate Formula", - "note": "" - }, - { - "seq": "MW", - "kind": "command", - "target": "formulas.watchWindow", - "excel": "Watch Window", - "note": "" - }, - { - "seq": "MX", - "kind": "continue", - "excel": "Calculation Options Menu", - "note": "group level: containment(Calculation Options)" - }, - { - "seq": "MXA", - "kind": "command", - "target": "formulas.calculationOptions", - "excel": "Automatic Calculation", - "note": "Calculation options: Automatic" - }, - { - "seq": "MXM", - "kind": "command", - "target": "formulas.calculationOptions", - "excel": "Manual Calculation", - "note": "Calculation options: Manual" - }, - { - "seq": "MXP", - "kind": "command", - "target": "formulas.calculationOptions", - "excel": "Partial Calculation", - "note": "Calculation options: Partial" - }, - { - "seq": "AB", - "kind": "dialog", - "target": "subtotal", - "excel": "Subtotal", - "note": "" - }, - { - "seq": "AC", - "kind": "command", - "target": "data.clearFilter", - "excel": "Clear Filter", - "note": "" - }, - { - "seq": "AD", - "kind": "command", - "target": "data.consolidate", - "excel": "Consolidate", - "note": "" - }, - { - "seq": "AE", - "kind": "dialog", - "target": "textToColumns", - "excel": "Text to Columns", - "note": "" - }, - { - "seq": "AF1", - "kind": "continue", - "excel": "Data from Picture Menu", - "note": "group level: feature gap; reserved until the command lands" - }, - { - "seq": "AF1C", - "kind": "command", - "target": "insert.picture", - "excel": "Picture from Clipboard (source TBD)" - }, - { - "seq": "AF1P", - "kind": "command", - "target": "insert.picture", - "excel": "Picture from File (source TBD)" - }, - { - "seq": "AFA", - "kind": "command", - "target": "data.fromAccess", - "pending": true, - "excel": "Get External Data: From Access", - "note": "gap #12 Get & Transform; reserved until the command lands" - }, - { - "seq": "AFC", - "kind": "command", - "target": "data.forecastSheet", - "pending": true, - "excel": "Forecast Sheet", - "note": "IN FLIGHT: vangel76 PR #325; reserved until the command lands" - }, - { - "seq": "AFE", - "kind": "command", - "target": "data.analyzeData", - "pending": true, - "excel": "Analyze Data", - "note": "AI feature GAP; reserved until the command lands" - }, - { - "seq": "AFF", - "kind": "command", - "target": "edit.flashFill", - "excel": "Flash Fill", - "note": "" - }, - { - "seq": "AFO", - "kind": "command", - "target": "data.fromOtherSources", - "pending": true, - "excel": "Get External Data: From Other Sources", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "AFT", - "kind": "command", - "target": "data.fromText", - "pending": true, - "excel": "Get External Data: From Text", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "AFW", - "kind": "command", - "target": "data.fromWeb", - "pending": true, - "excel": "Get External Data: From Web", - "note": "reserved until the command lands" - }, - { - "seq": "AG", - "kind": "continue", - "excel": "Group Menu", - "note": "group level: containment(Group)" - }, - { - "seq": "AGA", - "kind": "command", - "target": "data.autoOutline", - "pending": true, - "excel": "Auto Outline", - "note": "Auto Outline; reserved until the command lands" - }, - { - "seq": "AGG", - "kind": "command", - "target": "data.group", - "excel": "Group", - "note": "" - }, - { - "seq": "AH", - "kind": "command", - "target": "data.hideDetail", - "excel": "Hide Detail", - "note": "" - }, - { - "seq": "AJ", - "kind": "command", - "target": "data.showDetail", - "excel": "Show Detail", - "note": "" - }, - { - "seq": "AK", - "kind": "command", - "target": "data.queriesConnections", - "excel": "Edit Links", - "note": "Edit Links (Queries & Connections);" - }, - { - "seq": "AM", - "kind": "dialog", - "target": "removeDuplicates", - "excel": "Remove Duplicates", - "note": "" - }, - { - "seq": "AO", - "kind": "command", - "target": "data.queriesConnections", - "excel": "Queries & Connections", - "note": "" - }, - { - "seq": "AP1", - "kind": "command", - "target": "file.properties", - "excel": "Properties", - "note": "" - }, - { - "seq": "APN", - "kind": "continue", - "excel": "Get Data (Power Query) Menu", - "note": "group level: Power Query variants = GAP" - }, - { - "seq": "APND", - "kind": "command", - "target": "data.fromDatabase", - "pending": true, - "excel": "From Database (Microsoft Query)", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "APNE", - "kind": "command", - "target": "data.powerQueryEditor", - "pending": true, - "excel": "Launch Power Query Editor", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "APNG", - "kind": "command", - "target": "data.getData", - "excel": "Get Data (Power Query)", - "note": "containment(Get Data)" - }, - { - "seq": "APNH", - "kind": "command", - "target": "data.fromHtml", - "pending": true, - "excel": "From HTML", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "APNL", - "kind": "command", - "target": "data.fromSqlServer", - "pending": true, - "excel": "From SQL Server ODBC", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "APNS", - "kind": "command", - "target": "data.dataSourceSettings", - "pending": true, - "excel": "Data Source Settings", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "APNT", - "kind": "command", - "target": "data.fromTextLegacy", - "pending": true, - "excel": "From Text (Legacy)", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "APR", - "kind": "command", - "target": "data.recentSources", - "pending": true, - "excel": "Recent Sources", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "APS", - "kind": "command", - "target": "data.queriesConnections", - "excel": "Show Queries", - "note": "" - }, - { - "seq": "APT", - "kind": "command", - "target": "data.fromTable", - "pending": true, - "excel": "From Table (query)", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "AQ", - "kind": "command", - "target": "data.advancedFilter", - "excel": "Advanced Filter", - "note": "" - }, - { - "seq": "AR", - "kind": "continue", - "note": "group: group: Queries & Connections" - }, - { - "seq": "ARA", - "kind": "command", - "target": "data.refreshAll", - "excel": "Refresh All", - "note": "" - }, - { - "seq": "ARC", - "kind": "command", - "target": "data.cancelRefresh", - "pending": true, - "excel": "Cancel Refresh", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "ARO", - "kind": "command", - "target": "data.connectionProperties", - "pending": true, - "excel": "Connection Properties", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "ARR", - "kind": "command", - "target": "pivot.refresh", - "excel": "Refresh", - "note": "" - }, - { - "seq": "ARS", - "kind": "command", - "target": "data.refreshStatus", - "pending": true, - "excel": "Refresh Status", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "ASA", - "kind": "command", - "target": "data.sortAscending", - "excel": "Sort A to Z", - "note": "" - }, - { - "seq": "ASD", - "kind": "command", - "target": "data.sortDescending", - "excel": "Sort Z to A", - "note": "" - }, - { - "seq": "ASS", - "kind": "dialog", - "target": "sort", - "excel": "Sort", - "note": "" - }, - { - "seq": "AT", - "kind": "command", - "target": "data.filter", - "excel": "Filter", - "note": "" - }, - { - "seq": "AU", - "kind": "continue", - "excel": "Ungroup Menu", - "note": "group level: containment(Ungroup)" - }, - { - "seq": "AUC", - "kind": "command", - "target": "data.clearOutline", - "pending": true, - "excel": "Clear Outline", - "note": "feature gap; reserved until the command lands" - }, - { - "seq": "AUU", - "kind": "command", - "target": "data.ungroup", - "excel": "Ungroup", - "note": "" - }, - { - "seq": "AV", - "kind": "continue", - "excel": "Data Validation Menu", - "note": "group level: containment(Data Validation)" - }, - { - "seq": "AVI", - "kind": "command", - "target": "data.circleInvalid", - "excel": "Circle Invalid Data", - "note": "" - }, - { - "seq": "AVR", - "kind": "command", - "target": "data.clearValidationCircles", - "pending": true, - "excel": "Clear Validation Circles", - "note": "reserved until the command lands" - }, - { - "seq": "AVV", - "kind": "command", - "target": "data.validation", - "excel": "Data Validation", - "note": "" - }, - { - "seq": "AW", - "kind": "continue", - "note": "group: group: What-If (goalSeek/scenarioManager/dataTable)" - }, - { - "seq": "AWG", - "kind": "dialog", - "target": "goalSeek", - "excel": "Goal Seek", - "note": "" - }, - { - "seq": "AWS", - "kind": "command", - "target": "data.scenarioManager", - "excel": "Scenario Manager", - "note": "" - }, - { - "seq": "AWT", - "kind": "command", - "target": "data.dataTable", - "excel": "Data Table", - "note": "" - }, - { - "seq": "AX", - "kind": "command", - "target": "data.existingConnections", - "pending": true, - "excel": "Existing Connections", - "note": "gap #12; reserved until the command lands" - }, - { - "seq": "AY", - "kind": "command", - "target": "data.reapply", - "excel": "Reapply Filter", - "note": "containment(Reapply)" - }, - { - "seq": "RA1", - "kind": "dialog", - "target": "accessibility", - "excel": "Check Accessibility", - "note": "" - }, - { - "seq": "RA2", - "kind": "command", - "target": "review.showComments", - "pending": true, - "excel": "Show All Comments", - "note": "Show All Comments; reserved until the command lands" - }, - { - "seq": "RC", - "kind": "dialog", - "target": "comment", - "excel": "New Comment", - "note": "" - }, - { - "seq": "RD", - "kind": "command", - "target": "review.deleteComment", - "excel": "Delete Comment", - "note": "" - }, - { - "seq": "RG", - "kind": "command", - "target": "review.showChanges", - "pending": true, - "excel": "Track Changes", - "note": "reserved until the command lands" - }, - { - "seq": "RL", - "kind": "command", - "target": "review.translate", - "pending": true, - "excel": "Translate", - "note": "feature gap; reserved until the command lands" - }, - { - "seq": "RN", - "kind": "command", - "target": "review.nextComment", - "pending": true, - "excel": "Next Comment", - "note": "Next Comment; reserved until the command lands" - }, - { - "seq": "RO", - "kind": "command", - "target": "review.protectWorkbook", - "excel": "Protect and Share Workbook" - }, - { - "seq": "RPS", - "kind": "dialog", - "target": "protectSheet", - "excel": "Protect Sheet", - "note": "" - }, - { - "seq": "RPW", - "kind": "command", - "target": "review.protectWorkbook", - "excel": "Protect Workbook", - "note": "" - }, - { - "seq": "RRS", - "kind": "command", - "target": "review.smartLookup", - "pending": true, - "excel": "Smart Lookup", - "note": "feature gap; reserved until the command lands" - }, - { - "seq": "RS", - "kind": "dialog", - "target": "spelling", - "excel": "Spelling", - "note": "" - }, - { - "seq": "RTN", - "kind": "command", - "target": "review.newNote", - "excel": "New Note", - "note": "" - }, - { - "seq": "RV", - "kind": "command", - "target": "review.previousComment", - "pending": true, - "excel": "Previous Comment", - "note": "Previous Comment; reserved until the command lands" - }, - { - "seq": "RW", - "kind": "command", - "target": "review.shareWorkbook", - "pending": true, - "excel": "Share Workbook", - "note": "reserved until the command lands" - }, - { - "seq": "WA", - "kind": "command", - "target": "view.arrangeAll", - "excel": "Arrange All", - "note": "" - }, - { - "seq": "WB", - "kind": "command", - "target": "view.sideBySide", - "pending": true, - "excel": "View Side by Side", - "note": "reserved until the command lands" - }, - { - "seq": "WC", - "kind": "command", - "target": "view.customViews", - "excel": "Custom Views", - "note": "" - }, - { - "seq": "WF", - "kind": "continue", - "excel": "Freeze Panes Menu", - "note": "group level:" - }, - { - "seq": "WFC", - "kind": "command", - "target": "view.freezeFirstColumn", - "excel": "Freeze First Column", - "note": "" - }, - { - "seq": "WFF", - "kind": "command", - "target": "view.freezePanes", - "excel": "Freeze Panes", - "note": "" - }, - { - "seq": "WFR", - "kind": "command", - "target": "view.freezeTopRow", - "excel": "Freeze Top Row", - "note": "" - }, - { - "seq": "WG", - "kind": "command", - "target": "view.zoomToSelection", - "excel": "Zoom To Selection", - "note": "" - }, - { - "seq": "WH", - "kind": "command", - "target": "view.hideWindow", - "excel": "Hide Window", - "note": "" - }, - { - "seq": "WI", - "kind": "command", - "target": "view.pageBreakPreview", - "excel": "Page Break Preview", - "note": "" - }, - { - "seq": "WJ", - "kind": "control", - "target": "view.zoom", - "excel": "Zoom To 100%", - "note": "runs the 100% zoom handler (percent param)" - }, - { - "seq": "WL", - "kind": "command", - "target": "view.normal", - "excel": "Normal View", - "note": "containment(Normal)" - }, - { - "seq": "WM", - "kind": "continue", - "excel": "Macros Menu", - "note": "group level: containment(Macros)" - }, - { - "seq": "WMR", - "kind": "command", - "target": "view.macros", - "excel": "Record Macro", - "note": "Record Macro (macros menu)" - }, - { - "seq": "WMV", - "kind": "command", - "target": "view.macros", - "excel": "View Macros", - "note": "containment(Macros)" - }, - { - "seq": "WN", - "kind": "command", - "target": "view.newWindow", - "excel": "New Window", - "note": "" - }, - { - "seq": "WP", - "kind": "command", - "target": "view.pageLayout", - "excel": "Page Layout View", - "note": "" - }, - { - "seq": "WQ", - "kind": "dialog", - "target": "zoom", - "excel": "Zoom", - "note": "opens the Zoom dialog" - }, - { - "seq": "WR", - "kind": "command", - "target": "view.ruler", - "excel": "Ruler", - "note": "" - }, - { - "seq": "WS", - "kind": "command", - "target": "view.split", - "excel": "Split", - "note": "" - }, - { - "seq": "WT", - "kind": "command", - "target": "view.resetWindowPosition", - "pending": true, - "excel": "Reset Window Position", - "note": "reserved until the command lands" - }, - { - "seq": "WU", - "kind": "command", - "target": "view.unhideWindow", - "excel": "Unhide Window", - "note": "" - }, - { - "seq": "WVC", - "kind": "command", - "target": "view.focusCell", - "pending": true, - "excel": "Focus Cell Menu", - "note": "Focus Cell; reserved until the command lands" - }, - { - "seq": "WVF", - "kind": "control", - "target": "view.formulaBar", - "excel": "Toggle Formula Bar", - "note": "toggles the formula bar" - }, - { - "seq": "WVH", - "kind": "command", - "target": "view.headings", - "excel": "Toggle Headings", - "note": "containment(Headings)" - }, - { - "seq": "WVL", - "kind": "command", - "target": "view.synchronousScrolling", - "pending": true, - "excel": "Synchronous Scrolling", - "note": "Synchronous Scrolling; reserved until the command lands" - }, - { - "seq": "WVN", - "kind": "command", - "target": "view.navigation", - "pending": true, - "excel": "Navigation", - "note": "Navigation pane; reserved until the command lands" - }, - { - "seq": "WVR", - "kind": "command", - "target": "view.ruler", - "excel": "Ruler", - "note": "" - }, - { - "seq": "WVV", - "kind": "command", - "target": "view.sideBySide", - "pending": true, - "excel": "View Side by Side", - "note": "reserved until the command lands" - }, - { - "seq": "WW", - "kind": "command", - "target": "view.switchWindows", - "excel": "Switch Windows", - "note": "" - }, - { - "seq": "JAM", - "kind": "command", - "target": "chart.formatSelection", - "excel": "Chart format: Format Selection pane", - "note": "containment(Format Selection)" - }, - { - "seq": "JCA", - "kind": "command", - "target": "chart.addElement", - "excel": "Chart: Add Chart Element", - "note": "containment(Add Chart Element)" - }, - { - "seq": "JCC", - "kind": "command", - "target": "chart.changeType", - "excel": "Chart: Change Chart Type", - "note": "containment(Change Chart Type)" - }, - { - "seq": "JTS", - "kind": "command", - "target": "table.style", - "excel": "Table: table style", - "note": "" - }, - { - "seq": "JTV", - "kind": "command", - "target": "table.summarizePivot", - "excel": "Table: Summarize with PivotTable", - "note": "containment(Summarize with PivotTable)" - }, - { - "seq": "FA", - "kind": "command", - "excel": "Save / Save As page", - "note": "fires immediately in Excel; deeper aliases dropped as unreachable", - "target": "file.saveAs" - }, - { - "seq": "FC", - "kind": "command", - "target": "file.close", - "excel": "Close workbook" - }, - { - "seq": "FD", - "kind": "command", - "target": "file.account", - "pending": true, - "excel": "Account page", - "note": "Account page; reserved until the command lands" - }, - { - "seq": "FEA", - "kind": "command", - "target": "file.exportPdf", - "excel": "Export: create PDF/XPS", - "note": "Export: create PDF/XPS" - }, - { - "seq": "FEC", - "kind": "command", - "target": "file.saveAs", - "excel": "Export: change file type", - "note": "Export: change file type" - }, - { - "seq": "FH", - "kind": "command", - "target": "file.home", - "pending": true, - "excel": "File Home page (recent/pinned/shared/new blank)", - "note": "File Home page; reserved until the command lands" - }, - { - "seq": "FI", - "kind": "command", - "excel": "Info page", - "note": "fires immediately in Excel; deeper aliases dropped as unreachable", - "target": "file.properties" - }, - { - "seq": "FN", - "kind": "command", - "target": "file.new", - "excel": "New page", - "note": "New page" - }, - { - "seq": "FO", - "kind": "command", - "excel": "Open page", - "note": "fires immediately in Excel; deeper aliases dropped as unreachable", - "target": "file.open" - }, - { - "seq": "FP", - "kind": "command", - "target": "file.print", - "excel": "Print page", - "note": "containment(Print)" - }, - { - "seq": "FT", - "kind": "command", - "target": "file.options", - "pending": true, - "excel": "Options dialog", - "note": "Options dialog; reserved until the command lands" - }, - { - "seq": "Q", - "kind": "command", - "target": "ui.search", - "pending": true, - "excel": "Search / Tell Me", - "note": "Alt+Q search box does not exist yet (issue #49)" - }, - { - "seq": "6", - "kind": "command", - "target": "qat.6", - "pending": true, - "excel": "Quick Access Toolbar position 6", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "7", - "kind": "command", - "target": "qat.7", - "pending": true, - "excel": "Quick Access Toolbar position 7", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "8", - "kind": "command", - "target": "qat.8", - "pending": true, - "excel": "Quick Access Toolbar position 8", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "9", - "kind": "command", - "target": "qat.9", - "pending": true, - "excel": "Quick Access Toolbar position 9", - "note": "QAT navigation does not exist yet" - } - ] + "doc": [ + "Single source of truth for Excel-style sequential access keys (Alt+H+X+X style).", + "The generated keytip_table/generated.rs is produced from this file: `cargo xtask keytips`.", + "Adding or removing a shortcut is a one-line edit here. Rules:", + " - seq: the access-key sequence, letters and digits only (two-letter units like \"JI\" are one entry).", + " - kind: \"tab\" (switches a ribbon tab), \"command\" (runs a command id), \"dialog\" (opens a dialog by name),", + " \"continue\" (an explicit group level with no deeper sequences yet).", + " - Intermediate levels need no entries: the matcher waits automatically when deeper sequences exist.", + " - pending: true = the command does not exist yet; the sequence stays reserved and silent until it lands.", + " - Keep entries grouped by tab and sorted by sequence within a group; the FIRST entry for a tab is its canonical badge (legacy aliases come after it)." + ], + "sequences": [ + { + "seq": "H", + "kind": "tab", + "target": "Home" + }, + { + "seq": "N", + "kind": "tab", + "target": "Insert" + }, + { + "seq": "JI", + "kind": "tab", + "target": "Draw", + "note": "Two-letter unit. Deeper Draw letters differ between Excel and GridCraft; left blank for future contributors." + }, + { + "seq": "P", + "kind": "tab", + "target": "Page Layout" + }, + { + "seq": "M", + "kind": "tab", + "target": "Formulas" + }, + { + "seq": "A", + "kind": "tab", + "target": "Data" + }, + { + "seq": "R", + "kind": "tab", + "target": "Review" + }, + { + "seq": "W", + "kind": "tab", + "target": "View" + }, + { + "seq": "E", + "kind": "tab", + "target": "Home", + "note": "Legacy Edit-menu path (Excel 2003); leads to the same Home menus." + }, + { + "seq": "O", + "kind": "tab", + "target": "Home", + "note": "Legacy Format-menu path (Excel 2003); leads to the same Home menus." + }, + { + "seq": "HAR", + "kind": "command", + "target": "home.alignRight" + }, + { + "seq": "HAT", + "kind": "command", + "target": "home.alignTop", + "excel": "Excel: Top Align" + }, + { + "seq": "HAM", + "kind": "command", + "target": "home.alignMiddle", + "excel": "Excel: Middle Align" + }, + { + "seq": "HAB", + "kind": "command", + "target": "home.alignBottom", + "excel": "Excel: Bottom Align" + }, + { + "seq": "HAL", + "kind": "command", + "target": "home.alignLeft" + }, + { + "seq": "HAC", + "kind": "command", + "target": "home.alignCenter" + }, + { + "seq": "HVS", + "kind": "dialog", + "target": "pasteSpecial" + }, + { + "seq": "ES", + "kind": "dialog", + "target": "pasteSpecial", + "excel": "Classic Paste Special dialog (legacy Edit route); A/F/T/V are dialog mnemonics" + }, + { + "seq": "HOI", + "kind": "command", + "target": "home.autofitColumnWidth" + }, + { + "seq": "OCA", + "kind": "command", + "target": "home.autofitColumnWidth", + "note": "Legacy Format menu." + }, + { + "seq": "WVG", + "kind": "command", + "target": "view.gridlines" + }, + { + "seq": "0", + "kind": "command", + "target": "qat.10", + "pending": true, + "excel": "Quick Access Toolbar position 10", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "1", + "kind": "command", + "target": "qat.1", + "pending": true, + "excel": "Quick Access Toolbar position 1", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "2", + "kind": "command", + "target": "qat.2", + "pending": true, + "excel": "Quick Access Toolbar position 2", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "3", + "kind": "command", + "target": "qat.3", + "pending": true, + "excel": "Quick Access Toolbar position 3", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "4", + "kind": "command", + "target": "qat.4", + "pending": true, + "excel": "Quick Access Toolbar position 4", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "5", + "kind": "command", + "target": "qat.5", + "pending": true, + "excel": "Quick Access Toolbar position 5", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "H0", + "kind": "command", + "target": "home.increaseDecimal", + "excel": "Increase Decimals", + "note": "containment(Increase Decimal)" + }, + { + "seq": "H1", + "kind": "command", + "target": "home.bold", + "excel": "Bold", + "note": "" + }, + { + "seq": "H2", + "kind": "command", + "target": "home.italic", + "excel": "Italic", + "note": "" + }, + { + "seq": "H3", + "kind": "command", + "excel": "Excel: Underline; fires immediately (not a menu)", + "target": "home.underline" + }, + { + "seq": "H5", + "kind": "command", + "target": "home.decreaseIndent", + "excel": "Decrease Indent", + "note": "" + }, + { + "seq": "H6", + "kind": "command", + "target": "home.increaseIndent", + "excel": "Increase Indent", + "note": "" + }, + { + "seq": "H9", + "kind": "command", + "target": "home.decreaseDecimal", + "excel": "Decrease Decimals", + "note": "containment(Decrease Decimal)" + }, + { + "seq": "HAN", + "kind": "command", + "excel": "Accounting Format", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "home.accounting" + }, + { + "seq": "HAS", + "kind": "command", + "target": "home.addIns", + "excel": "Add-ins", + "note": "containment(Add-ins)" + }, + { + "seq": "HB", + "kind": "continue", + "excel": "Borders Menu", + "note": "group level:" + }, + { + "seq": "HBA", + "kind": "command", + "target": "home.borders", + "excel": "All Borders", + "note": "containment(Borders)" + }, + { + "seq": "HBB", + "kind": "command", + "target": "home.borders", + "excel": "Bottom Double Border", + "note": "border-side TBD" + }, + { + "seq": "HBC", + "kind": "command", + "target": "home.borders", + "excel": "Top and Thick Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBD", + "kind": "command", + "target": "home.borders", + "excel": "Top and Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBE", + "kind": "command", + "target": "home.borders", + "excel": "Erase Border", + "note": "border-side TBD" + }, + { + "seq": "HBG", + "kind": "command", + "target": "home.drawBorder", + "excel": "Draw Border Grid", + "note": "containment(Draw Border)" + }, + { + "seq": "HBH", + "kind": "command", + "target": "home.borders", + "excel": "Thick Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBI", + "kind": "command", + "target": "home.borders", + "excel": "Line Color", + "note": "Line Color (submenu TBD)" + }, + { + "seq": "HBL", + "kind": "command", + "target": "home.borders", + "excel": "Left Border", + "note": "border-side TBD" + }, + { + "seq": "HBM", + "kind": "command", + "target": "home.borders", + "excel": "More Borders", + "note": "containment(Borders)" + }, + { + "seq": "HBN", + "kind": "command", + "target": "home.borders", + "excel": "No Border", + "note": "border-side TBD" + }, + { + "seq": "HBO", + "kind": "command", + "target": "home.borders", + "excel": "Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBP", + "kind": "command", + "target": "home.borders", + "excel": "Top Border", + "note": "border-side TBD" + }, + { + "seq": "HBR", + "kind": "command", + "target": "home.borders", + "excel": "Right Border", + "note": "border-side TBD" + }, + { + "seq": "HBS", + "kind": "command", + "target": "home.borders", + "excel": "Outside Borders", + "note": "containment(Borders)" + }, + { + "seq": "HBT", + "kind": "command", + "target": "home.borders", + "excel": "Thick Outside Borders", + "note": "border-side TBD" + }, + { + "seq": "HBU", + "kind": "command", + "target": "home.borders", + "excel": "Top and Double Bottom Border", + "note": "border-side TBD" + }, + { + "seq": "HBW", + "kind": "command", + "target": "home.drawBorder", + "excel": "Draw Border", + "note": "" + }, + { + "seq": "HBY", + "kind": "command", + "target": "home.borders", + "excel": "Line Style", + "note": "Line Style (submenu TBD)" + }, + { + "seq": "HC", + "kind": "command", + "excel": "Copy", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "edit.copy" + }, + { + "seq": "HDC", + "kind": "command", + "target": "home.deleteColumns", + "excel": "Delete Columns", + "note": "Delete Columns" + }, + { + "seq": "HDD", + "kind": "dialog", + "target": "deleteCells", + "excel": "Delete Cells", + "note": "" + }, + { + "seq": "HDR", + "kind": "command", + "target": "home.deleteRows", + "excel": "Delete Rows", + "note": "Delete Rows" + }, + { + "seq": "HDS", + "kind": "command", + "target": "home.deleteSheet", + "excel": "Delete Sheet", + "note": "" + }, + { + "seq": "HEA", + "kind": "command", + "target": "edit.clearAll", + "excel": "Clear All", + "note": "" + }, + { + "seq": "HEC", + "kind": "command", + "target": "edit.clearContents", + "excel": "Clear Contents", + "note": "" + }, + { + "seq": "HEF", + "kind": "command", + "target": "edit.clearFormats", + "excel": "Clear Formats", + "note": "" + }, + { + "seq": "HEL", + "kind": "command", + "target": "edit.clearHyperlinks", + "excel": "Clear Hyperlinks", + "note": "" + }, + { + "seq": "HEM", + "kind": "command", + "target": "edit.clearComments", + "excel": "Clear Comments and Notes", + "note": "containment(Clear Comments and Notes)" + }, + { + "seq": "HFC", + "kind": "control", + "target": "home.fontColor", + "excel": "Font Color", + "note": "opens the font-color palette" + }, + { + "seq": "HFDC", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "Find Conditional Formatting", + "note": "containment(Conditional Formatting)" + }, + { + "seq": "HFDF", + "kind": "dialog", + "target": "find", + "excel": "Find", + "note": "" + }, + { + "seq": "HFDG", + "kind": "dialog", + "target": "goTo", + "excel": "Go To", + "note": "" + }, + { + "seq": "HFDM", + "kind": "command", + "target": "edit.find", + "excel": "Find Notes", + "note": "containment(Find)" + }, + { + "seq": "HFDO", + "kind": "command", + "target": "home.selectObjects", + "pending": true, + "excel": "Select Objects", + "note": "Select Objects; reserved until the command lands" + }, + { + "seq": "HFDP", + "kind": "command", + "target": "arrange.selectionPane", + "excel": "Selection Pane", + "note": "" + }, + { + "seq": "HFDR", + "kind": "dialog", + "target": "find", + "excel": "Replace", + "note": "opens Find and Replace" + }, + { + "seq": "HFDS", + "kind": "dialog", + "target": "goToSpecial", + "excel": "Go To Special", + "note": "" + }, + { + "seq": "HFF", + "kind": "control", + "target": "home.fontName", + "excel": "Font Selection", + "note": "opens the font dropdown" + }, + { + "seq": "HFG", + "kind": "command", + "target": "home.increaseFontSize", + "excel": "Increase Font Size", + "note": "" + }, + { + "seq": "HFI", + "kind": "continue", + "excel": "Fill", + "note": "group level: containment(Fill Up)" + }, + { + "seq": "HFIA", + "kind": "command", + "target": "home.fillAcrossWorksheets", + "pending": true, + "excel": "Fill Across Worksheets", + "note": "Fill Across Worksheets; reserved until the command lands" + }, + { + "seq": "HFID", + "kind": "command", + "target": "edit.fillDown", + "excel": "Fill Down", + "note": "" + }, + { + "seq": "HFIF", + "kind": "command", + "target": "edit.flashFill", + "excel": "Flash Fill", + "note": "" + }, + { + "seq": "HFIJ", + "kind": "command", + "target": "home.justify", + "excel": "Fill Justify", + "note": "containment(Justify)" + }, + { + "seq": "HFIL", + "kind": "command", + "target": "edit.fillLeft", + "excel": "Fill Left", + "note": "" + }, + { + "seq": "HFIR", + "kind": "command", + "target": "edit.fillRight", + "excel": "Fill Right", + "note": "" + }, + { + "seq": "HFIS", + "kind": "dialog", + "target": "series", + "excel": "Fill Series", + "note": "containment(Series)" + }, + { + "seq": "HFIU", + "kind": "command", + "target": "edit.fillUp", + "excel": "Fill Up", + "note": "" + }, + { + "seq": "HFK", + "kind": "command", + "target": "home.decreaseFontSize", + "excel": "Decrease Font Size", + "note": "" + }, + { + "seq": "HFM", + "kind": "dialog", + "target": "formatCells", + "excel": "More Number Formats", + "note": "More Number Formats opens Format Cells" + }, + { + "seq": "HFP", + "kind": "command", + "target": "edit.formatPainter", + "excel": "Format Painter", + "note": "" + }, + { + "seq": "HFQ", + "kind": "control", + "excel": "Orientation Menu", + "note": "group level: containment(Orientation)", + "target": "home.orientation" + }, + { + "seq": "HFS", + "kind": "control", + "target": "home.fontSize", + "excel": "Font Size", + "note": "opens the font-size dropdown" + }, + { + "seq": "HH", + "kind": "control", + "target": "home.fillColor", + "excel": "Fill Color", + "note": "opens the fill-color palette" + }, + { + "seq": "HIC", + "kind": "command", + "target": "home.insertColumns", + "excel": "Insert Columns", + "note": "Insert Columns" + }, + { + "seq": "HIE", + "kind": "dialog", + "target": "insertCells", + "excel": "Insert Copied Cells", + "note": "Insert Cells (copied cells TBD)" + }, + { + "seq": "HII", + "kind": "dialog", + "target": "insertCells", + "excel": "Insert Cells", + "note": "" + }, + { + "seq": "HIR", + "kind": "command", + "target": "home.insertRows", + "excel": "Insert Rows", + "note": "Insert Rows" + }, + { + "seq": "HIS", + "kind": "command", + "target": "home.insertSheet", + "excel": "Insert Sheet", + "note": "" + }, + { + "seq": "HJ", + "kind": "continue", + "excel": "Cell Styles", + "note": "group level:" + }, + { + "seq": "HJN", + "kind": "command", + "target": "home.newCellStyle", + "excel": "New Cell Style", + "note": "" + }, + { + "seq": "HK", + "kind": "command", + "target": "home.comma", + "excel": "Comma Style", + "note": "" + }, + { + "seq": "HL", + "kind": "continue", + "excel": "Conditional Formatting", + "note": "group level: containment(Conditional Formatting)" + }, + { + "seq": "HLC", + "kind": "continue", + "excel": "Clear Rules Menu", + "note": "group level: containment(Clear Rules)" + }, + { + "seq": "HLCE", + "kind": "command", + "target": "home.clearRules", + "excel": "Clear Rules From Sheet", + "note": "containment(Clear Rules)" + }, + { + "seq": "HLCS", + "kind": "command", + "target": "home.clearRules", + "excel": "Clear Rules From Cells", + "note": "containment(Clear Rules)" + }, + { + "seq": "HLCT", + "kind": "command", + "target": "home.clearRules", + "excel": "Clear Rules From Table", + "note": "containment(Clear Rules)" + }, + { + "seq": "HLD", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "Data Bars", + "note": "Data Bars (submenu TBD)" + }, + { + "seq": "HLH", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "Highlight Cells", + "note": "Highlight Cells (submenu TBD)" + }, + { + "seq": "HLN", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "New Rule", + "note": "New Rule (submenu TBD)" + }, + { + "seq": "HLR", + "kind": "command", + "target": "home.manageRules", + "excel": "Manage Rules", + "note": "containment(Manage Rules)" + }, + { + "seq": "HLT", + "kind": "command", + "target": "home.conditionalFormat", + "excel": "Top/Bottom Rules", + "note": "Top/Bottom (submenu TBD)" + }, + { + "seq": "HM", + "kind": "continue", + "excel": "Merge & Center Menu", + "note": "group level: containment(Merge & Center)" + }, + { + "seq": "HMA", + "kind": "command", + "target": "home.mergeAcross", + "excel": "Merge Across", + "note": "" + }, + { + "seq": "HMC", + "kind": "command", + "target": "home.mergeCenter", + "excel": "Merge & Center", + "note": "" + }, + { + "seq": "HMM", + "kind": "command", + "target": "home.mergeCells", + "excel": "Merge Cells", + "note": "" + }, + { + "seq": "HMU", + "kind": "command", + "target": "home.unmergeCells", + "excel": "Unmerge Cells", + "note": "" + }, + { + "seq": "HN", + "kind": "control", + "target": "home.numberFormat", + "excel": "Number Format", + "note": "opens the number-format dropdown" + }, + { + "seq": "HOA", + "kind": "command", + "target": "home.autofitRowHeight", + "excel": "AutoFit Row Height", + "note": "" + }, + { + "seq": "HOD", + "kind": "dialog", + "target": "defaultWidth", + "excel": "Default Column Width", + "note": "containment(Column Width)" + }, + { + "seq": "HOE", + "kind": "command", + "target": "home.formatCells", + "excel": "Format Cells", + "note": "" + }, + { + "seq": "HOH", + "kind": "dialog", + "target": "rowHeight", + "excel": "Row Height", + "note": "" + }, + { + "seq": "HOL", + "kind": "command", + "target": "home.lockCell", + "excel": "Lock Cell", + "note": "containment(Lock Cell)" + }, + { + "seq": "HOM", + "kind": "dialog", + "target": "moveSheet", + "excel": "Move or Copy Sheet", + "note": "" + }, + { + "seq": "HOP", + "kind": "dialog", + "target": "protectSheet", + "excel": "Protect Sheet", + "note": "" + }, + { + "seq": "HOR", + "kind": "dialog", + "target": "renameSheet", + "excel": "Rename Sheet", + "note": "" + }, + { + "seq": "HOT", + "kind": "command", + "target": "sheet.tabColor", + "excel": "Tab Color", + "note": "" + }, + { + "seq": "HOUC", + "kind": "command", + "target": "home.hideColumns", + "excel": "Hide Columns", + "note": "" + }, + { + "seq": "HOUH", + "kind": "command", + "target": "sheet.unhide", + "excel": "Unhide Sheet", + "note": "" + }, + { + "seq": "HOUL", + "kind": "command", + "target": "home.unhideColumns", + "excel": "Unhide Columns", + "note": "" + }, + { + "seq": "HOUO", + "kind": "command", + "target": "home.unhideRows", + "excel": "Unhide Rows", + "note": "" + }, + { + "seq": "HOUR", + "kind": "command", + "target": "home.hideRows", + "excel": "Hide Rows", + "note": "" + }, + { + "seq": "HOUS", + "kind": "command", + "target": "sheet.hide", + "excel": "Hide Sheet", + "note": "" + }, + { + "seq": "HOW", + "kind": "dialog", + "target": "columnWidth", + "excel": "Column Width", + "note": "" + }, + { + "seq": "HP", + "kind": "command", + "target": "home.percent", + "excel": "Excel: Percentage Style" + }, + { + "seq": "HSC", + "kind": "command", + "target": "data.clearFilter", + "excel": "Clear Filter", + "note": "" + }, + { + "seq": "HSF", + "kind": "command", + "target": "data.filter", + "excel": "Filter", + "note": "" + }, + { + "seq": "HSO", + "kind": "command", + "target": "data.sortDescending", + "excel": "Sort Z to A", + "note": "" + }, + { + "seq": "HSS", + "kind": "command", + "target": "data.sortAscending", + "excel": "Sort A to Z", + "note": "" + }, + { + "seq": "HSU", + "kind": "command", + "target": "data.sort", + "excel": "Custom Sort", + "note": "" + }, + { + "seq": "HT", + "kind": "continue", + "excel": "Format as Table", + "note": "group level:" + }, + { + "seq": "HTN", + "kind": "command", + "target": "table.style", + "excel": "New Table Style", + "note": "" + }, + { + "seq": "HTP", + "kind": "command", + "target": "table.style", + "excel": "New Pivot Table Style", + "note": "" + }, + { + "seq": "HU", + "kind": "continue", + "excel": "AutoSum", + "note": "group level:" + }, + { + "seq": "HUA", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Auto Average", + "note": "AutoSum: Average (variant TBD)" + }, + { + "seq": "HUC", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Auto Count Numbers", + "note": "AutoSum: Count (variant TBD)" + }, + { + "seq": "HUI", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Auto Min", + "note": "AutoSum: Min (variant TBD)" + }, + { + "seq": "HUM", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Auto Max", + "note": "AutoSum: Max (variant TBD)" + }, + { + "seq": "HV", + "kind": "continue", + "excel": "Paste", + "note": "group level:" + }, + { + "seq": "HVP", + "kind": "command", + "target": "edit.pasteSpecial", + "excel": "Paste", + "note": "" + }, + { + "seq": "HW", + "kind": "continue", + "excel": "Wrap Text Menu", + "note": "group level: containment(Wrap Text)" + }, + { + "seq": "HWW", + "kind": "command", + "target": "home.wrapText", + "excel": "Wrap Text", + "note": "" + }, + { + "seq": "HX", + "kind": "command", + "target": "edit.cut", + "excel": "Cut", + "note": "" + }, + { + "seq": "NC1", + "kind": "control", + "target": "insert.chart.column", + "excel": "Column Chart", + "note": "Column chart (kind TBD)" + }, + { + "seq": "NC2", + "kind": "command", + "target": "review.newComment", + "excel": "New Comment", + "note": "" + }, + { + "seq": "NCM", + "kind": "dialog", + "target": "comment", + "excel": "Excel: New Comment (modern)" + }, + { + "seq": "NCB", + "kind": "command", + "target": "insert.checkbox", + "excel": "Checkbox", + "note": "" + }, + { + "seq": "ND", + "kind": "control", + "target": "insert.chart.scatter", + "excel": "Scatter Chart", + "note": "Scatter (kind TBD)" + }, + { + "seq": "NE", + "kind": "command", + "target": "insert.equation", + "pending": true, + "excel": "Equations", + "note": "Equations; reserved until the command lands" + }, + { + "seq": "NF", + "kind": "continue", + "excel": "Online Pictures", + "note": "group level: containment(Picture)" + }, + { + "seq": "NFP", + "kind": "continue", + "excel": "From Picture Menu", + "note": "group level: containment(Picture)" + }, + { + "seq": "NFPF", + "kind": "command", + "target": "insert.picture", + "excel": "Picture From File", + "note": "containment(Picture)" + }, + { + "seq": "NH1", + "kind": "dialog", + "target": "headerFooter", + "excel": "Header & Footer", + "note": "" + }, + { + "seq": "NHF", + "kind": "command", + "target": "pageLayout.headerFooter", + "excel": "Header & Footer", + "note": "" + }, + { + "seq": "NHI", + "kind": "control", + "target": "insert.chart.hierarchy", + "excel": "Hierarchy Chart", + "note": "Hierarchy (kind TBD)" + }, + { + "seq": "NI1", + "kind": "command", + "target": "insert.chart", + "excel": "Waterfall Chart", + "note": "Waterfall (kind TBD)" + }, + { + "seq": "NI2", + "kind": "dialog", + "target": "insertLink", + "excel": "Link", + "note": "" + }, + { + "seq": "NJ", + "kind": "command", + "target": "insert.object", + "pending": true, + "excel": "Object", + "note": "insert.object missing; reserved until the command lands" + }, + { + "seq": "NM1T", + "kind": "command", + "target": "insert.picture", + "excel": "SmartArt Picture", + "note": "containment(Picture)" + }, + { + "seq": "NM2", + "kind": "command", + "target": "insert.chart", + "excel": "Map Chart", + "note": "Map (kind TBD)" + }, + { + "seq": "NN1", + "kind": "control", + "target": "insert.chart.line", + "excel": "Line Chart", + "note": "Line chart (kind TBD)" + }, + { + "seq": "NNS", + "kind": "command", + "target": "insert.icons", + "excel": "Icons", + "note": "" + }, + { + "seq": "NP", + "kind": "continue", + "excel": "Picture", + "note": "group level:" + }, + { + "seq": "NPB", + "kind": "command", + "target": "insert.picture", + "excel": "Picture from File", + "note": "containment(Picture)" + }, + { + "seq": "NPF", + "kind": "command", + "target": "insert.picture", + "excel": "Place in Cell", + "note": "Picture in cell (source TBD)" + }, + { + "seq": "NPI", + "kind": "command", + "target": "insert.picture", + "excel": "Place in Cell Menu", + "note": "Picture in cell (source TBD)" + }, + { + "seq": "NPO", + "kind": "command", + "target": "insert.picture", + "excel": "Place over Cells Menu", + "note": "Picture over cells (source TBD)" + }, + { + "seq": "NQ", + "kind": "control", + "target": "insert.chart.pie", + "excel": "Pie Chart", + "note": "Pie (kind TBD)" + }, + { + "seq": "NR", + "kind": "command", + "target": "insert.recommendedCharts", + "excel": "Recommended Charts", + "note": "" + }, + { + "seq": "NSA", + "kind": "control", + "target": "insert.chart.statistic", + "excel": "Statistical Chart", + "note": "Statistical chart (kind TBD)" + }, + { + "seq": "NSC", + "kind": "command", + "target": "insert.screenshot", + "pending": true, + "excel": "Screenshot Menu", + "note": "Screenshot; reserved until the command lands" + }, + { + "seq": "NSD", + "kind": "command", + "target": "insert.chart", + "excel": "Combo Chart", + "note": "Combo (kind TBD)" + }, + { + "seq": "NSF", + "kind": "command", + "target": "insert.slicer", + "pending": true, + "excel": "Slicer", + "note": "insert.slicer missing (gap #8); reserved until the command lands" + }, + { + "seq": "NSH", + "kind": "command", + "target": "insert.shape", + "excel": "Shapes", + "note": "" + }, + { + "seq": "NSP", + "kind": "command", + "target": "insert.recommendedPivotTables", + "excel": "Recommended PivotTables", + "note": "" + }, + { + "seq": "NST", + "kind": "command", + "target": "insert.timeline", + "pending": true, + "excel": "Timeline", + "note": "insert.timeline missing (gap #8); reserved until the command lands" + }, + { + "seq": "NSZ", + "kind": "command", + "target": "insert.pivotChart", + "pending": true, + "excel": "PivotChart", + "note": "PivotChart; reserved until the command lands" + }, + { + "seq": "NT", + "kind": "command", + "target": "insert.table", + "excel": "Table", + "note": "" + }, + { + "seq": "NU", + "kind": "command", + "target": "insert.symbol", + "excel": "Symbols", + "note": "containment(Symbol)" + }, + { + "seq": "NZS", + "kind": "command", + "target": "insert.symbol", + "excel": "Excel: Symbol (modern)" + }, + { + "seq": "NV", + "kind": "command", + "target": "insert.pivotTable", + "excel": "PivotTable", + "note": "" + }, + { + "seq": "NW", + "kind": "command", + "target": "insert.wordArt", + "pending": true, + "excel": "WordArt", + "note": "WordArt; reserved until the command lands" + }, + { + "seq": "NX", + "kind": "command", + "target": "insert.textBox", + "excel": "Text Box", + "note": "" + }, + { + "seq": "NZK", + "kind": "continue", + "excel": "Sparklines Menu", + "note": "group level:" + }, + { + "seq": "NZKSL", + "kind": "dialog", + "target": "sparkline", + "excel": "Line Sparkline", + "note": "" + }, + { + "seq": "NZKSO", + "kind": "dialog", + "target": "sparkline", + "excel": "Column Sparkline", + "note": "" + }, + { + "seq": "NZKSW", + "kind": "dialog", + "target": "sparkline", + "excel": "Win/Loss Sparkline", + "note": "" + }, + { + "seq": "NZTX", + "kind": "command", + "target": "insert.textBox", + "excel": "Text Box", + "note": "" + }, + { + "seq": "NZT", + "kind": "command", + "target": "insert.textBox", + "excel": "Excel: Text Box (modern)" + }, + { + "seq": "PAE", + "kind": "command", + "target": "arrange.sendBackward", + "excel": "Send Backward", + "note": "" + }, + { + "seq": "PAF", + "kind": "command", + "target": "arrange.bringForward", + "excel": "Bring Forward", + "note": "" + }, + { + "seq": "PB", + "kind": "continue", + "excel": "Breaks", + "note": "group level:" + }, + { + "seq": "PBI", + "kind": "command", + "target": "pageLayout.breaks", + "excel": "Insert Page Break", + "note": "Insert Page Break" + }, + { + "seq": "PBR", + "kind": "command", + "target": "pageLayout.breaks", + "excel": "Remove Page Break", + "note": "Remove Page Break" + }, + { + "seq": "PGB", + "kind": "command", + "target": "pageLayout.background", + "excel": "Background", + "note": "Excel badge GB (Background in the Page Setup group)" + }, + { + "seq": "PI", + "kind": "command", + "target": "pageLayout.printTitles", + "excel": "Print Titles", + "note": "" + }, + { + "seq": "PM", + "kind": "continue", + "excel": "Margins", + "note": "group level:" + }, + { + "seq": "PMA", + "kind": "command", + "target": "pageLayout.margins", + "excel": "Custom Margins", + "note": "containment(Margins)" + }, + { + "seq": "PO", + "kind": "command", + "target": "pageLayout.orientation", + "excel": "Orientation", + "note": "" + }, + { + "seq": "PPG", + "kind": "command", + "target": "pageLayout.printGridlines", + "excel": "Print Gridlines", + "note": "" + }, + { + "seq": "PPH", + "kind": "command", + "target": "pageLayout.printHeadings", + "excel": "Print Headings", + "note": "" + }, + { + "seq": "PR", + "kind": "continue", + "excel": "Print Area", + "note": "group level:" + }, + { + "seq": "PRS", + "kind": "command", + "target": "pageLayout.printArea", + "excel": "Set Print Area", + "note": "" + }, + { + "seq": "PSP", + "kind": "dialog", + "target": "pageSetup", + "excel": "Page Setup", + "note": "" + }, + { + "seq": "PSZ", + "kind": "command", + "target": "pageLayout.size", + "excel": "Size", + "note": "" + }, + { + "seq": "PTC", + "kind": "command", + "target": "pageLayout.themeColors", + "excel": "Theme Colors", + "note": "containment(Colors)" + }, + { + "seq": "PTE", + "kind": "command", + "target": "pageLayout.themeEffects", + "excel": "Theme Effects", + "note": "containment(Effects)" + }, + { + "seq": "PTF", + "kind": "command", + "target": "pageLayout.themeFonts", + "excel": "Theme Fonts", + "note": "containment(Fonts)" + }, + { + "seq": "PTH", + "kind": "continue", + "excel": "Themes", + "note": "group level:" + }, + { + "seq": "PTHB", + "kind": "command", + "target": "pageLayout.theme", + "excel": "Browse Themes", + "note": "containment(Themes)" + }, + { + "seq": "PVG", + "kind": "command", + "target": "view.gridlines", + "excel": "View Gridlines", + "note": "" + }, + { + "seq": "PVH", + "kind": "command", + "target": "view.headings", + "excel": "View Headings", + "note": "" + }, + { + "seq": "MA", + "kind": "continue", + "excel": "Remove Arrows Menu", + "note": "group level: containment(Remove Arrows)" + }, + { + "seq": "MAA", + "kind": "command", + "target": "formulas.removeArrows", + "excel": "Remove All Arrows", + "note": "landed in the catalog; live since T7 probe" + }, + { + "seq": "MAP", + "kind": "command", + "target": "formulas.removePrecedentArrows", + "pending": true, + "excel": "Remove Precedent Arrows", + "note": "Remove Precedent Arrows; reserved until the command lands" + }, + { + "seq": "MB", + "kind": "command", + "target": "formulas.calculateNow", + "excel": "Calculate Now", + "note": "" + }, + { + "seq": "MC", + "kind": "command", + "target": "formulas.createFromSelection", + "excel": "Create From Selection", + "note": "" + }, + { + "seq": "MD", + "kind": "command", + "target": "formulas.traceDependents", + "excel": "Trace Dependents", + "note": "" + }, + { + "seq": "ME", + "kind": "control", + "excel": "Date & Time Menu", + "note": "group level: containment(Date & Time)", + "target": "formulas.dateTime" + }, + { + "seq": "MF", + "kind": "dialog", + "target": "insertFunction", + "excel": "Insert Function", + "note": "" + }, + { + "seq": "MG", + "kind": "control", + "excel": "Math & Trig Menu", + "note": "group level: containment(Math & Trig)", + "target": "formulas.mathTrig" + }, + { + "seq": "MH", + "kind": "command", + "target": "formulas.showFormulas", + "excel": "Show Formulas", + "note": "" + }, + { + "seq": "MI", + "kind": "continue", + "excel": "Financial Menu", + "note": "group level: containment(Financial)" + }, + { + "seq": "MI1", + "kind": "control", + "target": "formulas.financial", + "excel": "Excel: Financial; first item of the dropdown" + }, + { + "seq": "MJ", + "kind": "command", + "target": "formulas.calculateSheet", + "excel": "Calculate Sheet", + "note": "" + }, + { + "seq": "MK", + "kind": "continue", + "excel": "Error Checking Menu", + "note": "group level: containment(Error Checking)" + }, + { + "seq": "MKK", + "kind": "dialog", + "target": "errorChecking", + "excel": "Error Checking", + "note": "" + }, + { + "seq": "ML", + "kind": "control", + "excel": "Logical Menu", + "note": "group level: containment(Logical)", + "target": "formulas.logical" + }, + { + "seq": "MM", + "kind": "dialog", + "target": "defineName", + "excel": "Define Name", + "note": "" + }, + { + "seq": "MN", + "kind": "dialog", + "target": "nameManager", + "excel": "Name Manager", + "note": "" + }, + { + "seq": "MO", + "kind": "control", + "excel": "Lookup & Reference Menu", + "note": "group level: containment(Lookup & Reference)", + "target": "formulas.lookup" + }, + { + "seq": "MP", + "kind": "command", + "target": "formulas.tracePrecedents", + "excel": "Trace Precedents", + "note": "" + }, + { + "seq": "MQ", + "kind": "control", + "excel": "More Functions Menu", + "note": "group level: containment(More Functions)", + "target": "formulas.moreFunctions" + }, + { + "seq": "MS", + "kind": "continue", + "excel": "Use in Formula Menu", + "note": "group level: containment(Use in Formula)" + }, + { + "seq": "MSP", + "kind": "command", + "target": "formulas.pasteName", + "excel": "Paste Names", + "note": "reserved until the command lands", + "pending": true + }, + { + "seq": "MT", + "kind": "control", + "excel": "Text Menu", + "note": "group level: containment(Text)", + "target": "formulas.text" + }, + { + "seq": "MU", + "kind": "continue", + "excel": "AutoSum Menu", + "note": "group level:" + }, + { + "seq": "MUA", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Average", + "note": "AutoSum: Average (variant TBD)" + }, + { + "seq": "MUC", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Count Numbers", + "note": "AutoSum: Count (variant TBD)" + }, + { + "seq": "MUF", + "kind": "command", + "target": "formulas.moreFunctions", + "excel": "More Functions (AutoSum)", + "note": "containment(More Functions)" + }, + { + "seq": "MUI", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Min", + "note": "AutoSum: Min (variant TBD)" + }, + { + "seq": "MUM", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Max", + "note": "AutoSum: Max (variant TBD)" + }, + { + "seq": "MUS", + "kind": "command", + "target": "formulas.autoSum", + "excel": "Sum", + "note": "" + }, + { + "seq": "MV", + "kind": "dialog", + "target": "evaluateFormula", + "excel": "Evaluate Formula", + "note": "" + }, + { + "seq": "MW", + "kind": "command", + "target": "formulas.watchWindow", + "excel": "Watch Window", + "note": "" + }, + { + "seq": "MX", + "kind": "continue", + "excel": "Calculation Options Menu", + "note": "group level: containment(Calculation Options)" + }, + { + "seq": "MXA", + "kind": "command", + "target": "formulas.calculationOptions", + "excel": "Automatic Calculation", + "note": "Calculation options: Automatic" + }, + { + "seq": "MXM", + "kind": "command", + "target": "formulas.calculationOptions", + "excel": "Manual Calculation", + "note": "Calculation options: Manual" + }, + { + "seq": "MXP", + "kind": "command", + "target": "formulas.calculationOptions", + "excel": "Partial Calculation", + "note": "Calculation options: Partial" + }, + { + "seq": "AB", + "kind": "dialog", + "target": "subtotal", + "excel": "Subtotal", + "note": "" + }, + { + "seq": "AC", + "kind": "command", + "target": "data.clearFilter", + "excel": "Clear Filter", + "note": "" + }, + { + "seq": "AD", + "kind": "command", + "target": "data.consolidate", + "excel": "Consolidate", + "note": "" + }, + { + "seq": "AE", + "kind": "dialog", + "target": "textToColumns", + "excel": "Text to Columns", + "note": "" + }, + { + "seq": "AF1", + "kind": "continue", + "excel": "Data from Picture Menu", + "note": "group level: feature gap; reserved until the command lands" + }, + { + "seq": "AF1C", + "kind": "command", + "target": "insert.picture", + "excel": "Picture from Clipboard (source TBD)" + }, + { + "seq": "AF1P", + "kind": "command", + "target": "insert.picture", + "excel": "Picture from File (source TBD)" + }, + { + "seq": "AFA", + "kind": "command", + "target": "data.fromAccess", + "pending": true, + "excel": "Get External Data: From Access", + "note": "gap #12 Get & Transform; reserved until the command lands" + }, + { + "seq": "AFC", + "kind": "command", + "target": "data.forecastSheet", + "pending": true, + "excel": "Forecast Sheet", + "note": "IN FLIGHT: vangel76 PR #325; reserved until the command lands" + }, + { + "seq": "AFE", + "kind": "command", + "target": "data.analyzeData", + "pending": true, + "excel": "Analyze Data", + "note": "AI feature GAP; reserved until the command lands" + }, + { + "seq": "AFF", + "kind": "command", + "target": "edit.flashFill", + "excel": "Flash Fill", + "note": "" + }, + { + "seq": "AFO", + "kind": "command", + "target": "data.fromOtherSources", + "pending": true, + "excel": "Get External Data: From Other Sources", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "AFT", + "kind": "command", + "target": "data.fromText", + "pending": true, + "excel": "Get External Data: From Text", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "AFW", + "kind": "command", + "target": "data.fromWeb", + "pending": true, + "excel": "Get External Data: From Web", + "note": "reserved until the command lands" + }, + { + "seq": "AG", + "kind": "continue", + "excel": "Group Menu", + "note": "group level: containment(Group)" + }, + { + "seq": "AGA", + "kind": "command", + "target": "data.autoOutline", + "pending": true, + "excel": "Auto Outline", + "note": "Auto Outline; reserved until the command lands" + }, + { + "seq": "AGG", + "kind": "command", + "target": "data.group", + "excel": "Group", + "note": "" + }, + { + "seq": "AH", + "kind": "command", + "target": "data.hideDetail", + "excel": "Hide Detail", + "note": "" + }, + { + "seq": "AJ", + "kind": "command", + "target": "data.showDetail", + "excel": "Show Detail", + "note": "" + }, + { + "seq": "AK", + "kind": "command", + "target": "data.queriesConnections", + "excel": "Edit Links", + "note": "Edit Links (Queries & Connections);" + }, + { + "seq": "AM", + "kind": "dialog", + "target": "removeDuplicates", + "excel": "Remove Duplicates", + "note": "" + }, + { + "seq": "AO", + "kind": "command", + "target": "data.queriesConnections", + "excel": "Queries & Connections", + "note": "" + }, + { + "seq": "AP1", + "kind": "command", + "target": "file.properties", + "excel": "Properties", + "note": "" + }, + { + "seq": "APN", + "kind": "continue", + "excel": "Get Data (Power Query) Menu", + "note": "group level: Power Query variants = GAP" + }, + { + "seq": "APND", + "kind": "command", + "target": "data.fromDatabase", + "pending": true, + "excel": "From Database (Microsoft Query)", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNE", + "kind": "command", + "target": "data.powerQueryEditor", + "pending": true, + "excel": "Launch Power Query Editor", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNG", + "kind": "command", + "target": "data.getData", + "excel": "Get Data (Power Query)", + "note": "containment(Get Data)" + }, + { + "seq": "APNH", + "kind": "command", + "target": "data.fromHtml", + "pending": true, + "excel": "From HTML", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNL", + "kind": "command", + "target": "data.fromSqlServer", + "pending": true, + "excel": "From SQL Server ODBC", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNS", + "kind": "command", + "target": "data.dataSourceSettings", + "pending": true, + "excel": "Data Source Settings", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APNT", + "kind": "command", + "target": "data.fromTextLegacy", + "pending": true, + "excel": "From Text (Legacy)", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APR", + "kind": "command", + "target": "data.recentSources", + "pending": true, + "excel": "Recent Sources", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "APS", + "kind": "command", + "target": "data.queriesConnections", + "excel": "Show Queries", + "note": "" + }, + { + "seq": "APT", + "kind": "command", + "target": "data.fromTable", + "pending": true, + "excel": "From Table (query)", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "AQ", + "kind": "command", + "target": "data.advancedFilter", + "excel": "Advanced Filter", + "note": "" + }, + { + "seq": "AR", + "kind": "continue", + "note": "group: group: Queries & Connections" + }, + { + "seq": "ARA", + "kind": "command", + "target": "data.refreshAll", + "excel": "Refresh All", + "note": "" + }, + { + "seq": "ARC", + "kind": "command", + "target": "data.cancelRefresh", + "pending": true, + "excel": "Cancel Refresh", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "ARO", + "kind": "command", + "target": "data.connectionProperties", + "pending": true, + "excel": "Connection Properties", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "ARR", + "kind": "command", + "target": "pivot.refresh", + "excel": "Refresh", + "note": "" + }, + { + "seq": "ARS", + "kind": "command", + "target": "data.refreshStatus", + "pending": true, + "excel": "Refresh Status", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "ASA", + "kind": "command", + "target": "data.sortAscending", + "excel": "Sort A to Z", + "note": "" + }, + { + "seq": "ASD", + "kind": "command", + "target": "data.sortDescending", + "excel": "Sort Z to A", + "note": "" + }, + { + "seq": "ASS", + "kind": "dialog", + "target": "sort", + "excel": "Sort", + "note": "" + }, + { + "seq": "AT", + "kind": "command", + "target": "data.filter", + "excel": "Filter", + "note": "" + }, + { + "seq": "AU", + "kind": "continue", + "excel": "Ungroup Menu", + "note": "group level: containment(Ungroup)" + }, + { + "seq": "AUC", + "kind": "command", + "target": "data.clearOutline", + "pending": true, + "excel": "Clear Outline", + "note": "feature gap; reserved until the command lands" + }, + { + "seq": "AUU", + "kind": "command", + "target": "data.ungroup", + "excel": "Ungroup", + "note": "" + }, + { + "seq": "AV", + "kind": "continue", + "excel": "Data Validation Menu", + "note": "group level: containment(Data Validation)" + }, + { + "seq": "AVI", + "kind": "command", + "target": "data.circleInvalid", + "excel": "Circle Invalid Data", + "note": "" + }, + { + "seq": "AVR", + "kind": "command", + "target": "data.clearValidationCircles", + "pending": true, + "excel": "Clear Validation Circles", + "note": "reserved until the command lands" + }, + { + "seq": "AVV", + "kind": "command", + "target": "data.validation", + "excel": "Data Validation", + "note": "" + }, + { + "seq": "AW", + "kind": "continue", + "note": "group: group: What-If (goalSeek/scenarioManager/dataTable)" + }, + { + "seq": "AWG", + "kind": "dialog", + "target": "goalSeek", + "excel": "Goal Seek", + "note": "" + }, + { + "seq": "AWS", + "kind": "command", + "target": "data.scenarioManager", + "excel": "Scenario Manager", + "note": "" + }, + { + "seq": "AWT", + "kind": "command", + "target": "data.dataTable", + "excel": "Data Table", + "note": "" + }, + { + "seq": "AX", + "kind": "command", + "target": "data.existingConnections", + "pending": true, + "excel": "Existing Connections", + "note": "gap #12; reserved until the command lands" + }, + { + "seq": "AY", + "kind": "command", + "target": "data.reapply", + "excel": "Reapply Filter", + "note": "containment(Reapply)" + }, + { + "seq": "RA1", + "kind": "dialog", + "target": "accessibility", + "excel": "Check Accessibility", + "note": "" + }, + { + "seq": "RA2", + "kind": "command", + "target": "review.showComments", + "pending": true, + "excel": "Show All Comments", + "note": "Show All Comments; reserved until the command lands" + }, + { + "seq": "RC", + "kind": "dialog", + "target": "comment", + "excel": "New Comment", + "note": "" + }, + { + "seq": "RD", + "kind": "command", + "target": "review.deleteComment", + "excel": "Delete Comment", + "note": "" + }, + { + "seq": "RG", + "kind": "command", + "target": "review.showChanges", + "pending": true, + "excel": "Track Changes", + "note": "reserved until the command lands" + }, + { + "seq": "RL", + "kind": "command", + "target": "review.translate", + "pending": true, + "excel": "Translate", + "note": "feature gap; reserved until the command lands" + }, + { + "seq": "RN", + "kind": "command", + "target": "review.nextComment", + "pending": true, + "excel": "Next Comment", + "note": "Next Comment; reserved until the command lands" + }, + { + "seq": "RO", + "kind": "command", + "target": "review.protectWorkbook", + "excel": "Protect and Share Workbook" + }, + { + "seq": "RPS", + "kind": "dialog", + "target": "protectSheet", + "excel": "Protect Sheet", + "note": "" + }, + { + "seq": "RPW", + "kind": "command", + "target": "review.protectWorkbook", + "excel": "Protect Workbook", + "note": "" + }, + { + "seq": "RRS", + "kind": "command", + "target": "review.smartLookup", + "pending": true, + "excel": "Smart Lookup", + "note": "feature gap; reserved until the command lands" + }, + { + "seq": "RS", + "kind": "dialog", + "target": "spelling", + "excel": "Spelling", + "note": "" + }, + { + "seq": "RTN", + "kind": "command", + "target": "review.newNote", + "excel": "New Note", + "note": "" + }, + { + "seq": "RV", + "kind": "command", + "target": "review.previousComment", + "pending": true, + "excel": "Previous Comment", + "note": "Previous Comment; reserved until the command lands" + }, + { + "seq": "RW", + "kind": "command", + "target": "review.shareWorkbook", + "pending": true, + "excel": "Share Workbook", + "note": "reserved until the command lands" + }, + { + "seq": "WA", + "kind": "command", + "target": "view.arrangeAll", + "excel": "Arrange All", + "note": "" + }, + { + "seq": "WB", + "kind": "command", + "target": "view.sideBySide", + "pending": true, + "excel": "View Side by Side", + "note": "reserved until the command lands" + }, + { + "seq": "WC", + "kind": "command", + "target": "view.customViews", + "excel": "Custom Views", + "note": "" + }, + { + "seq": "WF", + "kind": "continue", + "excel": "Freeze Panes Menu", + "note": "group level:" + }, + { + "seq": "WFC", + "kind": "command", + "target": "view.freezeFirstColumn", + "excel": "Freeze First Column", + "note": "" + }, + { + "seq": "WFF", + "kind": "command", + "target": "view.freezePanes", + "excel": "Freeze Panes", + "note": "" + }, + { + "seq": "WFR", + "kind": "command", + "target": "view.freezeTopRow", + "excel": "Freeze Top Row", + "note": "" + }, + { + "seq": "WG", + "kind": "command", + "target": "view.zoomToSelection", + "excel": "Zoom To Selection", + "note": "" + }, + { + "seq": "WH", + "kind": "command", + "target": "view.hideWindow", + "excel": "Hide Window", + "note": "" + }, + { + "seq": "WI", + "kind": "command", + "target": "view.pageBreakPreview", + "excel": "Page Break Preview", + "note": "" + }, + { + "seq": "WJ", + "kind": "control", + "target": "view.zoom", + "excel": "Zoom To 100%", + "note": "runs the 100% zoom handler (percent param)" + }, + { + "seq": "WL", + "kind": "command", + "target": "view.normal", + "excel": "Normal View", + "note": "containment(Normal)" + }, + { + "seq": "WM", + "kind": "continue", + "excel": "Macros Menu", + "note": "group level: containment(Macros)" + }, + { + "seq": "WMR", + "kind": "command", + "target": "view.macros", + "excel": "Record Macro", + "note": "Record Macro (macros menu)" + }, + { + "seq": "WMV", + "kind": "command", + "target": "view.macros", + "excel": "View Macros", + "note": "containment(Macros)" + }, + { + "seq": "WN", + "kind": "command", + "target": "view.newWindow", + "excel": "New Window", + "note": "" + }, + { + "seq": "WP", + "kind": "command", + "target": "view.pageLayout", + "excel": "Page Layout View", + "note": "" + }, + { + "seq": "WQ", + "kind": "dialog", + "target": "zoom", + "excel": "Zoom", + "note": "opens the Zoom dialog" + }, + { + "seq": "WR", + "kind": "command", + "target": "view.ruler", + "excel": "Ruler", + "note": "" + }, + { + "seq": "WS", + "kind": "command", + "target": "view.split", + "excel": "Split", + "note": "" + }, + { + "seq": "WT", + "kind": "command", + "target": "view.resetWindowPosition", + "pending": true, + "excel": "Reset Window Position", + "note": "reserved until the command lands" + }, + { + "seq": "WU", + "kind": "command", + "target": "view.unhideWindow", + "excel": "Unhide Window", + "note": "" + }, + { + "seq": "WVC", + "kind": "command", + "target": "view.focusCell", + "pending": true, + "excel": "Focus Cell Menu", + "note": "Focus Cell; reserved until the command lands" + }, + { + "seq": "WVF", + "kind": "control", + "target": "view.formulaBar", + "excel": "Toggle Formula Bar", + "note": "toggles the formula bar" + }, + { + "seq": "WVH", + "kind": "command", + "target": "view.headings", + "excel": "Toggle Headings", + "note": "containment(Headings)" + }, + { + "seq": "WVL", + "kind": "command", + "target": "view.synchronousScrolling", + "pending": true, + "excel": "Synchronous Scrolling", + "note": "Synchronous Scrolling; reserved until the command lands" + }, + { + "seq": "WVN", + "kind": "command", + "target": "view.navigation", + "pending": true, + "excel": "Navigation", + "note": "Navigation pane; reserved until the command lands" + }, + { + "seq": "WVR", + "kind": "command", + "target": "view.ruler", + "excel": "Ruler", + "note": "" + }, + { + "seq": "WVV", + "kind": "command", + "target": "view.sideBySide", + "pending": true, + "excel": "View Side by Side", + "note": "reserved until the command lands" + }, + { + "seq": "WW", + "kind": "command", + "target": "view.switchWindows", + "excel": "Switch Windows", + "note": "" + }, + { + "seq": "JAM", + "kind": "command", + "target": "chart.formatSelection", + "excel": "Chart format: Format Selection pane", + "note": "containment(Format Selection)" + }, + { + "seq": "JCA", + "kind": "command", + "target": "chart.addElement", + "excel": "Chart: Add Chart Element", + "note": "containment(Add Chart Element)" + }, + { + "seq": "JCC", + "kind": "command", + "target": "chart.changeType", + "excel": "Chart: Change Chart Type", + "note": "containment(Change Chart Type)" + }, + { + "seq": "JTS", + "kind": "command", + "target": "table.style", + "excel": "Table: table style", + "note": "" + }, + { + "seq": "JTV", + "kind": "command", + "target": "table.summarizePivot", + "excel": "Table: Summarize with PivotTable", + "note": "containment(Summarize with PivotTable)" + }, + { + "seq": "FA", + "kind": "command", + "excel": "Save / Save As page", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "file.saveAs" + }, + { + "seq": "FC", + "kind": "command", + "target": "file.close", + "excel": "Close workbook" + }, + { + "seq": "FD", + "kind": "command", + "target": "file.account", + "pending": true, + "excel": "Account page", + "note": "Account page; reserved until the command lands" + }, + { + "seq": "FEA", + "kind": "command", + "target": "file.exportPdf", + "excel": "Export: create PDF/XPS", + "note": "Export: create PDF/XPS" + }, + { + "seq": "FEC", + "kind": "command", + "target": "file.saveAs", + "excel": "Export: change file type", + "note": "Export: change file type" + }, + { + "seq": "FH", + "kind": "command", + "target": "file.home", + "pending": true, + "excel": "File Home page (recent/pinned/shared/new blank)", + "note": "File Home page; reserved until the command lands" + }, + { + "seq": "FI", + "kind": "command", + "excel": "Info page", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "file.properties" + }, + { + "seq": "FN", + "kind": "command", + "target": "file.new", + "excel": "New page", + "note": "New page" + }, + { + "seq": "FO", + "kind": "command", + "excel": "Open page", + "note": "fires immediately in Excel; deeper aliases dropped as unreachable", + "target": "file.open" + }, + { + "seq": "FP", + "kind": "command", + "target": "file.print", + "excel": "Print page", + "note": "containment(Print)" + }, + { + "seq": "FT", + "kind": "command", + "target": "file.options", + "pending": true, + "excel": "Options dialog", + "note": "Options dialog; reserved until the command lands" + }, + { + "seq": "Q", + "kind": "command", + "target": "ui.search", + "pending": true, + "excel": "Search / Tell Me", + "note": "Alt+Q search box does not exist yet (issue #49)" + }, + { + "seq": "6", + "kind": "command", + "target": "qat.6", + "pending": true, + "excel": "Quick Access Toolbar position 6", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "7", + "kind": "command", + "target": "qat.7", + "pending": true, + "excel": "Quick Access Toolbar position 7", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "8", + "kind": "command", + "target": "qat.8", + "pending": true, + "excel": "Quick Access Toolbar position 8", + "note": "QAT navigation does not exist yet" + }, + { + "seq": "9", + "kind": "command", + "target": "qat.9", + "pending": true, + "excel": "Quick Access Toolbar position 9", + "note": "QAT navigation does not exist yet" + } + ] } \ No newline at end of file diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 5ca18f8..e8291ac 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -393,10 +393,28 @@ fn editable_combo( if open_now && !st.was_open { st.text = String::new(); st.sel = items.iter().position(|(v, _)| *v == current_value).map(|i| i as i32).unwrap_or(0); + // The reset lands on the frame AFTER the popup opens, which usually has no input of + // its own — without a repaint request the stale filtered list stays on screen. + ui.ctx().request_repaint(); } st.was_open = open_now; let mut result = ComboResult::None; egui::Popup::menu(anchor).open_memory(popup_command(forced, anchor.clicked())).show(|ui| { + // Arrows and Enter navigate the list, not the filter field's cursor. Capture them and + // strip them from the input so the TextEdit never sees them. + let (mut down, mut up, mut enter) = (false, false, false); + ui.input(|i| { + down = i.key_pressed(egui::Key::ArrowDown); + up = i.key_pressed(egui::Key::ArrowUp); + enter = i.key_pressed(egui::Key::Enter); + }); + if down || up || enter { + ui.ctx().input_mut(|i| { + i.events.retain(|e| { + !matches!(e, egui::Event::Key { key: egui::Key::ArrowDown | egui::Key::ArrowUp | egui::Key::Enter, pressed: true, .. }) + }); + }); + } let text_id = anchor.id.with(salt).with("__combo_text"); let te = ui.add(egui::TextEdit::singleline(&mut st.text).id(text_id).desired_width(220.0).hint_text("Type to filter…")); if open_now && !te.has_focus() { @@ -410,28 +428,26 @@ fn editable_combo( } let lower = st.text.to_lowercase(); let filtered: Vec = items.iter().enumerate().filter(|(_, (_, d))| d.to_lowercase().contains(&lower)).map(|(i, _)| i).collect(); - ui.input(|i| { - if !filtered.is_empty() { - let n = filtered.len() as i32; - if i.key_pressed(egui::Key::ArrowDown) { - st.sel = (st.sel + 1).rem_euclid(n); - } - if i.key_pressed(egui::Key::ArrowUp) { - st.sel = (st.sel - 1).rem_euclid(n); - } + if !filtered.is_empty() { + let n = filtered.len() as i32; + if down { + st.sel = (st.sel + 1).rem_euclid(n); } - if i.key_pressed(egui::Key::Enter) { - if filtered.is_empty() { - let t = st.text.trim().to_string(); - if !t.is_empty() { - result = ComboResult::Entered(t); - } - } else { - let idx = filtered[st.sel.clamp(0, filtered.len() as i32 - 1) as usize]; - result = ComboResult::Picked(items[idx].0.to_string()); - } + if up { + st.sel = (st.sel - 1).rem_euclid(n); } - }); + } + if enter { + if filtered.is_empty() { + let t = st.text.trim().to_string(); + if !t.is_empty() { + result = ComboResult::Entered(t); + } + } else { + let idx = filtered[st.sel.clamp(0, filtered.len() as i32 - 1) as usize]; + result = ComboResult::Picked(items[idx].0.to_string()); + } + } ui.separator(); egui::ScrollArea::vertical().max_height(240.0).show(ui, |ui| { for i in &filtered { @@ -1679,23 +1695,31 @@ fn page_layout(app: &mut SheetApp, ui: &mut Ui) { ui.vertical(|ui| { ui.label(egui::RichText::new(app.ui.language.tr("Gridlines")).strong().small()); let mut v = sh.0; - if ui.checkbox(&mut v, screen).changed() { - act(app, "view.gridlines", json!({"on": v})); + let resp = ui.checkbox(&mut v, screen); + control_keytip(app, ui, &resp, "view.gridlines"); + if resp.changed() || invoke(app, "view.gridlines") { + act(app, "view.gridlines", json!({"on": !v})); } let mut p = sh.2; - if ui.checkbox(&mut p, print).changed() { - act(app, "pageLayout.printGridlines", json!({"on": p})); + let resp = ui.checkbox(&mut p, print); + control_keytip(app, ui, &resp, "pageLayout.printGridlines"); + if resp.changed() || invoke(app, "pageLayout.printGridlines") { + act(app, "pageLayout.printGridlines", json!({"on": !p})); } }); ui.vertical(|ui| { ui.label(egui::RichText::new(app.ui.language.tr("Headings")).strong().small()); let mut v = sh.1; - if ui.checkbox(&mut v, screen).changed() { - act(app, "view.headings", json!({"on": v})); + let resp = ui.checkbox(&mut v, screen); + control_keytip(app, ui, &resp, "view.headings"); + if resp.changed() || invoke(app, "view.headings") { + act(app, "view.headings", json!({"on": !v})); } let mut p = sh.3; - if ui.checkbox(&mut p, print).changed() { - act(app, "pageLayout.printHeadings", json!({"on": p})); + let resp = ui.checkbox(&mut p, print); + control_keytip(app, ui, &resp, "pageLayout.printHeadings"); + if resp.changed() || invoke(app, "pageLayout.printHeadings") { + act(app, "pageLayout.printHeadings", json!({"on": !p})); } }); let rtl = app.session.active().and_then(|d| d.wb.active()).is_some_and(|s| s.right_to_left); diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index 0f35966..b398222 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -495,6 +495,29 @@ fn editable_combos_filter_by_typing_and_move_by_arrows() { h.run_steps(3); assert!(h.state().message.is_none() && h.state().toast.is_none(), "custom sizes must apply"); assert_eq!(h.query_all_by_label("13.5").count(), 1, "the button must show the custom size"); + + // Closing without applying (Escape) and reopening must reset the filter to the full list. + let mut h = harness(blank()); + sequence(&mut h, &[Key::H, Key::F, Key::F], false); + h.run_steps(3); + events(&mut h, [Event::Text("arial".into())]); + h.run_steps(3); + key(&mut h, Key::Escape); + h.run_steps(3); + sequence(&mut h, &[Key::H, Key::F, Key::F], false); + h.run_steps(4); + assert!(h.query_all_by_label("Georgia").count() >= 1, "reopening must show the full, unfiltered list"); + // Arrows after typing move the highlight, not the text cursor: type a filter, + // then Down twice and Enter must apply the second match's neighbour logic. + key(&mut h, Key::Escape); + h.run_steps(3); + sequence(&mut h, &[Key::H, Key::F, Key::F], false); + h.run_steps(3); + events(&mut h, [Event::Text("candara".into())]); + h.run_steps(3); + key(&mut h, Key::Enter); + h.run_steps(3); + assert_eq!(h.query_all_by_label("Candara").count(), 1, "filtered Enter must apply the filtered row"); } #[test] diff --git a/crates/ui-egui/tests/keytips_generated.rs b/crates/ui-egui/tests/keytips_generated.rs index edc0109..5d4139d 100644 --- a/crates/ui-egui/tests/keytips_generated.rs +++ b/crates/ui-egui/tests/keytips_generated.rs @@ -1534,10 +1534,10 @@ fn keytip_pbr_() { } #[test] -fn keytip_pg_() { +fn keytip_pgb_() { let mut h = harness(blank()); - sequence(&mut h, "PG", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.background"), "PG must journal pageLayout.background"); + sequence(&mut h, "PGB", false); + assert!(h.state().session.journal.iter().any(|(id, _)| id == "pageLayout.background"), "PGB must journal pageLayout.background"); } #[test] From 8e7e2475892c20e1e443a518686c8b59d19667a7 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 23:20:22 +0300 Subject: [PATCH 13/15] Resolve James's real-Excel checks: drop QAT/Backstage and nonexistent View sequences; arrow navigation in the colour palette - Side-by-side and synchronous-scrolling keytips removed (do not exist in James's Excel); Copy H,C / Spelling R,S / Fill H,H / Sparklines N,Z,K confirmed already correct - QAT digits 0-9 and Backstage account/home/options + legacy Share Workbook sequences dropped (no meaningful GridCraft target; not left as trash) - Colour palette (H,H fill and F,C font colour): arrow keys move a highlight through the swatches, Enter applies; key events are stripped so the grid behind the popup does not react --- .../src/keytips/keytip_table/generated.rs | 47 ++--- crates/ui-egui/src/keytips/keytips.json | 136 -------------- crates/ui-egui/src/widgets.rs | 100 ++++++++-- crates/ui-egui/tests/keytips.rs | 15 ++ crates/ui-egui/tests/keytips_generated.rs | 172 +----------------- 5 files changed, 120 insertions(+), 350 deletions(-) diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index 9a95b01..100a20f 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -27,18 +27,12 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("HOI", Entry::Finish(Action::Command("home.autofitColumnWidth"))), ("OCA", Entry::Finish(Action::Command("home.autofitColumnWidth"))), // Legacy Format menu. ("WVG", Entry::Finish(Action::Command("view.gridlines"))), - ("0", Entry::Finish(Action::Pending("qat.10"))), // pending; Excel: Quick Access Toolbar position 10; QAT navigation does not exist yet - ("1", Entry::Finish(Action::Pending("qat.1"))), // pending; Excel: Quick Access Toolbar position 1; QAT navigation does not exist yet - ("2", Entry::Finish(Action::Pending("qat.2"))), // pending; Excel: Quick Access Toolbar position 2; QAT navigation does not exist yet - ("3", Entry::Finish(Action::Pending("qat.3"))), // pending; Excel: Quick Access Toolbar position 3; QAT navigation does not exist yet - ("4", Entry::Finish(Action::Pending("qat.4"))), // pending; Excel: Quick Access Toolbar position 4; QAT navigation does not exist yet - ("5", Entry::Finish(Action::Pending("qat.5"))), // pending; Excel: Quick Access Toolbar position 5; QAT navigation does not exist yet ("H0", Entry::Finish(Action::Command("home.increaseDecimal"))), // Excel: Increase Decimals; containment(Increase Decimal) - ("H1", Entry::Finish(Action::Command("home.bold"))), // Excel: Bold - ("H2", Entry::Finish(Action::Command("home.italic"))), // Excel: Italic - ("H3", Entry::Finish(Action::Command("home.underline"))), // Excel: Excel: Underline; fires immediately (not a menu) - ("H5", Entry::Finish(Action::Command("home.decreaseIndent"))), // Excel: Decrease Indent - ("H6", Entry::Finish(Action::Command("home.increaseIndent"))), // Excel: Increase Indent + ("H1", Entry::Finish(Action::Command("home.bold"))), // Excel: Bold + ("H2", Entry::Finish(Action::Command("home.italic"))), // Excel: Italic + ("H3", Entry::Finish(Action::Command("home.underline"))), // Excel: Excel: Underline; fires immediately (not a menu) + ("H5", Entry::Finish(Action::Command("home.decreaseIndent"))), // Excel: Decrease Indent + ("H6", Entry::Finish(Action::Command("home.increaseIndent"))), // Excel: Increase Indent ("H9", Entry::Finish(Action::Command("home.decreaseDecimal"))), // Excel: Decrease Decimals; containment(Decrease Decimal) ("HAN", Entry::Finish(Action::Command("home.accounting"))), // Excel: Accounting Format; fires immediately in Excel; deeper aliases dropped as unreachable ("HAS", Entry::Finish(Action::Command("home.addIns"))), // Excel: Add-ins; containment(Add-ins) @@ -335,9 +329,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("RS", Entry::Finish(Action::Dialog("spelling"))), // Excel: Spelling ("RTN", Entry::Finish(Action::Command("review.newNote"))), // Excel: New Note ("RV", Entry::Finish(Action::Pending("review.previousComment"))), // pending; Excel: Previous Comment; Previous Comment; reserved until the command lands - ("RW", Entry::Finish(Action::Pending("review.shareWorkbook"))), // pending; Excel: Share Workbook; reserved until the command lands ("WA", Entry::Finish(Action::Command("view.arrangeAll"))), // Excel: Arrange All - ("WB", Entry::Finish(Action::Pending("view.sideBySide"))), // pending; Excel: View Side by Side; reserved until the command lands ("WC", Entry::Finish(Action::Command("view.customViews"))), // Excel: Custom Views ("WF", Entry::Continue), // Excel: Freeze Panes Menu; group level: ("WFC", Entry::Finish(Action::Command("view.freezeFirstColumn"))), // Excel: Freeze First Column @@ -361,30 +353,21 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("WVC", Entry::Finish(Action::Pending("view.focusCell"))), // pending; Excel: Focus Cell Menu; Focus Cell; reserved until the command lands ("WVF", Entry::Finish(Action::Control("view.formulaBar"))), // Excel: Toggle Formula Bar; toggles the formula bar ("WVH", Entry::Finish(Action::Command("view.headings"))), // Excel: Toggle Headings; containment(Headings) - ("WVL", Entry::Finish(Action::Pending("view.synchronousScrolling"))), // pending; Excel: Synchronous Scrolling; Synchronous Scrolling; reserved until the command lands - ("WVN", Entry::Finish(Action::Pending("view.navigation"))), // pending; Excel: Navigation; Navigation pane; reserved until the command lands - ("WVR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler - ("WVV", Entry::Finish(Action::Pending("view.sideBySide"))), // pending; Excel: View Side by Side; reserved until the command lands - ("WW", Entry::Finish(Action::Command("view.switchWindows"))), // Excel: Switch Windows + ("WVN", Entry::Finish(Action::Pending("view.navigation"))), // pending; Excel: Navigation; Navigation pane; reserved until the command lands + ("WVR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler + ("WW", Entry::Finish(Action::Command("view.switchWindows"))), // Excel: Switch Windows ("JAM", Entry::Finish(Action::Command("chart.formatSelection"))), // Excel: Chart format: Format Selection pane; containment(Format Selection) - ("JCA", Entry::Finish(Action::Command("chart.addElement"))), // Excel: Chart: Add Chart Element; containment(Add Chart Element) - ("JCC", Entry::Finish(Action::Command("chart.changeType"))), // Excel: Chart: Change Chart Type; containment(Change Chart Type) - ("JTS", Entry::Finish(Action::Command("table.style"))), // Excel: Table: table style + ("JCA", Entry::Finish(Action::Command("chart.addElement"))), // Excel: Chart: Add Chart Element; containment(Add Chart Element) + ("JCC", Entry::Finish(Action::Command("chart.changeType"))), // Excel: Chart: Change Chart Type; containment(Change Chart Type) + ("JTS", Entry::Finish(Action::Command("table.style"))), // Excel: Table: table style ("JTV", Entry::Finish(Action::Command("table.summarizePivot"))), // Excel: Table: Summarize with PivotTable; containment(Summarize with PivotTable) ("FA", Entry::Finish(Action::Command("file.saveAs"))), // Excel: Save / Save As page; fires immediately in Excel; deeper aliases dropped as unreachable ("FC", Entry::Finish(Action::Command("file.close"))), // Excel: Close workbook - ("FD", Entry::Finish(Action::Pending("file.account"))), // pending; Excel: Account page; Account page; reserved until the command lands ("FEA", Entry::Finish(Action::Command("file.exportPdf"))), // Excel: Export: create PDF/XPS; Export: create PDF/XPS ("FEC", Entry::Finish(Action::Command("file.saveAs"))), // Excel: Export: change file type; Export: change file type - ("FH", Entry::Finish(Action::Pending("file.home"))), // pending; Excel: File Home page (recent/pinned/shared/new blank); File Home page; reserved until the command lands ("FI", Entry::Finish(Action::Command("file.properties"))), // Excel: Info page; fires immediately in Excel; deeper aliases dropped as unreachable - ("FN", Entry::Finish(Action::Command("file.new"))), // Excel: New page; New page - ("FO", Entry::Finish(Action::Command("file.open"))), // Excel: Open page; fires immediately in Excel; deeper aliases dropped as unreachable - ("FP", Entry::Finish(Action::Command("file.print"))), // Excel: Print page; containment(Print) - ("FT", Entry::Finish(Action::Pending("file.options"))), // pending; Excel: Options dialog; Options dialog; reserved until the command lands - ("Q", Entry::Finish(Action::Pending("ui.search"))), // pending; Excel: Search / Tell Me; Alt+Q search box does not exist yet (issue #49) - ("6", Entry::Finish(Action::Pending("qat.6"))), // pending; Excel: Quick Access Toolbar position 6; QAT navigation does not exist yet - ("7", Entry::Finish(Action::Pending("qat.7"))), // pending; Excel: Quick Access Toolbar position 7; QAT navigation does not exist yet - ("8", Entry::Finish(Action::Pending("qat.8"))), // pending; Excel: Quick Access Toolbar position 8; QAT navigation does not exist yet - ("9", Entry::Finish(Action::Pending("qat.9"))), // pending; Excel: Quick Access Toolbar position 9; QAT navigation does not exist yet + ("FN", Entry::Finish(Action::Command("file.new"))), // Excel: New page; New page + ("FO", Entry::Finish(Action::Command("file.open"))), // Excel: Open page; fires immediately in Excel; deeper aliases dropped as unreachable + ("FP", Entry::Finish(Action::Command("file.print"))), // Excel: Print page; containment(Print) + ("Q", Entry::Finish(Action::Pending("ui.search"))), // pending; Excel: Search / Tell Me; Alt+Q search box does not exist yet (issue #49) ]; diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index dd04f7b..18f41c9 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -124,54 +124,6 @@ "kind": "command", "target": "view.gridlines" }, - { - "seq": "0", - "kind": "command", - "target": "qat.10", - "pending": true, - "excel": "Quick Access Toolbar position 10", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "1", - "kind": "command", - "target": "qat.1", - "pending": true, - "excel": "Quick Access Toolbar position 1", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "2", - "kind": "command", - "target": "qat.2", - "pending": true, - "excel": "Quick Access Toolbar position 2", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "3", - "kind": "command", - "target": "qat.3", - "pending": true, - "excel": "Quick Access Toolbar position 3", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "4", - "kind": "command", - "target": "qat.4", - "pending": true, - "excel": "Quick Access Toolbar position 4", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "5", - "kind": "command", - "target": "qat.5", - "pending": true, - "excel": "Quick Access Toolbar position 5", - "note": "QAT navigation does not exist yet" - }, { "seq": "H0", "kind": "command", @@ -2282,14 +2234,6 @@ "excel": "Previous Comment", "note": "Previous Comment; reserved until the command lands" }, - { - "seq": "RW", - "kind": "command", - "target": "review.shareWorkbook", - "pending": true, - "excel": "Share Workbook", - "note": "reserved until the command lands" - }, { "seq": "WA", "kind": "command", @@ -2297,14 +2241,6 @@ "excel": "Arrange All", "note": "" }, - { - "seq": "WB", - "kind": "command", - "target": "view.sideBySide", - "pending": true, - "excel": "View Side by Side", - "note": "reserved until the command lands" - }, { "seq": "WC", "kind": "command", @@ -2466,14 +2402,6 @@ "excel": "Toggle Headings", "note": "containment(Headings)" }, - { - "seq": "WVL", - "kind": "command", - "target": "view.synchronousScrolling", - "pending": true, - "excel": "Synchronous Scrolling", - "note": "Synchronous Scrolling; reserved until the command lands" - }, { "seq": "WVN", "kind": "command", @@ -2489,14 +2417,6 @@ "excel": "Ruler", "note": "" }, - { - "seq": "WVV", - "kind": "command", - "target": "view.sideBySide", - "pending": true, - "excel": "View Side by Side", - "note": "reserved until the command lands" - }, { "seq": "WW", "kind": "command", @@ -2552,14 +2472,6 @@ "target": "file.close", "excel": "Close workbook" }, - { - "seq": "FD", - "kind": "command", - "target": "file.account", - "pending": true, - "excel": "Account page", - "note": "Account page; reserved until the command lands" - }, { "seq": "FEA", "kind": "command", @@ -2574,14 +2486,6 @@ "excel": "Export: change file type", "note": "Export: change file type" }, - { - "seq": "FH", - "kind": "command", - "target": "file.home", - "pending": true, - "excel": "File Home page (recent/pinned/shared/new blank)", - "note": "File Home page; reserved until the command lands" - }, { "seq": "FI", "kind": "command", @@ -2610,14 +2514,6 @@ "excel": "Print page", "note": "containment(Print)" }, - { - "seq": "FT", - "kind": "command", - "target": "file.options", - "pending": true, - "excel": "Options dialog", - "note": "Options dialog; reserved until the command lands" - }, { "seq": "Q", "kind": "command", @@ -2625,38 +2521,6 @@ "pending": true, "excel": "Search / Tell Me", "note": "Alt+Q search box does not exist yet (issue #49)" - }, - { - "seq": "6", - "kind": "command", - "target": "qat.6", - "pending": true, - "excel": "Quick Access Toolbar position 6", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "7", - "kind": "command", - "target": "qat.7", - "pending": true, - "excel": "Quick Access Toolbar position 7", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "8", - "kind": "command", - "target": "qat.8", - "pending": true, - "excel": "Quick Access Toolbar position 8", - "note": "QAT navigation does not exist yet" - }, - { - "seq": "9", - "kind": "command", - "target": "qat.9", - "pending": true, - "excel": "Quick Access Toolbar position 9", - "note": "QAT navigation does not exist yet" } ] } \ No newline at end of file diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index a006c6b..e24105b 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -145,9 +145,83 @@ pub fn committed_text(ui: &mut Ui, id: egui::Id, current: &str, width: Option Option { let mut picked = None; + const ROWS: usize = 7; // 6 tint rows + 1 standard row + const COLS: usize = 10; + let pal_id = ui.id().with("__color_palette"); + let mut cur: Option<(usize, usize)> = ui.data_mut(|d| d.get_temp(pal_id)).unwrap_or(None); + let (mut dl, mut dr, mut du, mut dd, mut ent) = (false, false, false, false, false); + ui.input(|i| { + dl = i.key_pressed(egui::Key::ArrowLeft); + dr = i.key_pressed(egui::Key::ArrowRight); + du = i.key_pressed(egui::Key::ArrowUp); + dd = i.key_pressed(egui::Key::ArrowDown); + ent = i.key_pressed(egui::Key::Enter); + }); + if dl || dr || du || dd { + cur = match cur { + None => Some(( + 0, + if dr { + 1 + } else if dl { + COLS - 1 + } else { + 0 + }, + )), + Some((r, c)) => { + let r2 = if du { + r.saturating_sub(1) + } else if dd { + (r + 1).min(ROWS - 1) + } else { + r + }; + let c2 = if dl { + (c + COLS - 1) % COLS + } else if dr { + (c + 1) % COLS + } else { + c + }; + Some((r2, c2)) + } + }; + } + if dl || dr || du || dd || ent { + ui.ctx().input_mut(|i| { + i.events.retain(|e| { + !matches!( + e, + egui::Event::Key { + key: egui::Key::ArrowLeft | egui::Key::ArrowRight | egui::Key::ArrowUp | egui::Key::ArrowDown | egui::Key::Enter, + pressed: true, + .. + } + ) + }); + }); + } + let theme_hex = |base: u32, tint: f64| -> String { + let rgb = [(base >> 16) as u8, (base >> 8) as u8, base as u8]; + let c = gridcraft_engine::model::style::apply_tint(rgb, tint); + let (r0, r1, r2) = (c[0], c[1], c[2]); + format!("#{r0:02X}{r1:02X}{r2:02X}") + }; + let std_hex = ["#C00000", "#FF0000", "#FFC000", "#FFFF00", "#92D050", "#00B050", "#00B0F0", "#0070C0", "#002060", "#7030A0"]; + if ent && let Some((r, c)) = cur { + picked = Some(if r < ROWS - 1 { + let base = theme_colors.get(c).copied().unwrap_or(0); + theme_hex(base, [0.0f64, 0.8, 0.6, 0.4, -0.25, -0.5][r]) + } else { + std_hex[c].to_string() + }); + ui.data_mut(|d| d.remove_temp::>(pal_id)); + } if ui.button(none_label).clicked() { picked = Some("none".to_string()); } @@ -155,20 +229,20 @@ pub fn color_palette(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str) -> let order = [0usize, 1, 2, 3, 4, 5, 6, 7, 8, 9]; let tints = [0.0f64, 0.8, 0.6, 0.4, -0.25, -0.5]; egui::Grid::new(ui.id().with("theme_grid")).spacing(vec2(2.0, 2.0)).show(ui, |ui| { - for tint in tints { - for &i in &order { + for (row, tint) in tints.iter().enumerate() { + for (col, &i) in order.iter().enumerate() { let base = theme_colors.get(i).copied().unwrap_or(0); let rgb = [(base >> 16) as u8, (base >> 8) as u8, base as u8]; - let c = gridcraft_engine::model::style::apply_tint(rgb, tint); - let col = Color32::from_rgb(c[0], c[1], c[2]); + let c = gridcraft_engine::model::style::apply_tint(rgb, *tint); + let col32 = Color32::from_rgb(c[0], c[1], c[2]); let (r, resp) = ui.allocate_exact_size(vec2(16.0, 14.0), Sense::click()); - ui.painter().rect_filled(r, 1.0, col); + ui.painter().rect_filled(r, 1.0, col32); ui.painter().rect_stroke(r, 1.0, Stroke::new(1.0, Color32::from_gray(200)), StrokeKind::Inside); - if resp.hovered() { + if resp.hovered() || cur == Some((row, col)) { ui.painter().rect_stroke(r.expand(1.0), 1.0, Stroke::new(1.5, Color32::from_rgb(0xE8, 0x7D, 0x2B)), StrokeKind::Outside); } if resp.clicked() { - picked = Some(format!("#{:02X}{:02X}{:02X}", c[0], c[1], c[2])); + picked = Some(theme_hex(base, *tint)); } } ui.end_row(); @@ -177,18 +251,22 @@ pub fn color_palette(ui: &mut Ui, theme_colors: &[u32; 12], none_label: &str) -> ui.label(egui::RichText::new("Standard Colors").small()); ui.horizontal(|ui| { ui.spacing_mut().item_spacing.x = 2.0; - for hex in ["#C00000", "#FF0000", "#FFC000", "#FFFF00", "#92D050", "#00B050", "#00B0F0", "#0070C0", "#002060", "#7030A0"] { - let col = gridcraft_engine::model::Color::from_hex(hex) + for (col, hex) in std_hex.iter().enumerate() { + let c = gridcraft_engine::model::Color::from_hex(hex) .and_then(|c| c.resolve(&Default::default())) .map(theme::color32) .unwrap_or(Color32::BLACK); let (r, resp) = ui.allocate_exact_size(vec2(16.0, 14.0), Sense::click()); - ui.painter().rect_filled(r, 1.0, col); + ui.painter().rect_filled(r, 1.0, c); + if resp.hovered() || cur == Some((ROWS - 1, col)) { + ui.painter().rect_stroke(r.expand(1.0), 1.0, Stroke::new(1.5, Color32::from_rgb(0xE8, 0x7D, 0x2B)), StrokeKind::Outside); + } if resp.clicked() { picked = Some(hex.to_string()); } } }); + ui.data_mut(|d| d.insert_temp(pal_id, cur)); picked } diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index b398222..b526782 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -520,6 +520,21 @@ fn editable_combos_filter_by_typing_and_move_by_arrows() { assert_eq!(h.query_all_by_label("Candara").count(), 1, "filtered Enter must apply the filtered row"); } +#[test] +fn color_palette_navigates_with_arrows_and_enter_applies() { + let mut h = harness(blank()); + sequence(&mut h, &[Key::H, Key::H], false); // H,H opens the fill palette + h.run_steps(3); + key(&mut h, Key::ArrowDown); // highlight starts at theme row 0 + key(&mut h, Key::ArrowRight); // -> row 0, col 1 = Craft theme dk1 (#000000) + key(&mut h, Key::Enter); + h.run_steps(3); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "arrow+Enter must not error"); + assert_eq!(h.state().session.journal.iter().filter(|(id, _)| id == "home.fillColor").count(), 1, "Enter applies the highlighted swatch"); + assert_eq!(h.state().grid.last_fill, "#000000", "the applied fill is the highlighted colour"); + assert_eq!(h.query_all_by_label("Theme Colors").count(), 0, "the palette closes after applying"); +} + #[test] fn every_sequence_terminates_without_panicking() { use gridcraft_ui_egui::keytips::keytip_table::SEQUENCES; diff --git a/crates/ui-egui/tests/keytips_generated.rs b/crates/ui-egui/tests/keytips_generated.rs index 5d4139d..dd71255 100644 --- a/crates/ui-egui/tests/keytips_generated.rs +++ b/crates/ui-egui/tests/keytips_generated.rs @@ -206,66 +206,6 @@ fn keytip_wvg_() { assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.gridlines"), "WVG must journal view.gridlines"); } -#[test] -fn keytip_0_() { - let mut h = harness(blank()); - sequence(&mut h, "0", false); - assert!(h.state().dialog.is_none(), "0 must not open a dialog"); - assert!(h.state().message.is_none(), "0 must not open a message box"); - assert!(h.state().toast.is_none(), "0 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "0 must not journal"); -} - -#[test] -fn keytip_1_() { - let mut h = harness(blank()); - sequence(&mut h, "1", false); - assert!(h.state().dialog.is_none(), "1 must not open a dialog"); - assert!(h.state().message.is_none(), "1 must not open a message box"); - assert!(h.state().toast.is_none(), "1 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "1 must not journal"); -} - -#[test] -fn keytip_2_() { - let mut h = harness(blank()); - sequence(&mut h, "2", false); - assert!(h.state().dialog.is_none(), "2 must not open a dialog"); - assert!(h.state().message.is_none(), "2 must not open a message box"); - assert!(h.state().toast.is_none(), "2 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "2 must not journal"); -} - -#[test] -fn keytip_3_() { - let mut h = harness(blank()); - sequence(&mut h, "3", false); - assert!(h.state().dialog.is_none(), "3 must not open a dialog"); - assert!(h.state().message.is_none(), "3 must not open a message box"); - assert!(h.state().toast.is_none(), "3 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "3 must not journal"); -} - -#[test] -fn keytip_4_() { - let mut h = harness(blank()); - sequence(&mut h, "4", false); - assert!(h.state().dialog.is_none(), "4 must not open a dialog"); - assert!(h.state().message.is_none(), "4 must not open a message box"); - assert!(h.state().toast.is_none(), "4 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "4 must not journal"); -} - -#[test] -fn keytip_5_() { - let mut h = harness(blank()); - sequence(&mut h, "5", false); - assert!(h.state().dialog.is_none(), "5 must not open a dialog"); - assert!(h.state().message.is_none(), "5 must not open a message box"); - assert!(h.state().toast.is_none(), "5 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "5 must not journal"); -} - #[test] fn keytip_h0_() { let mut h = harness(blank()); @@ -2516,16 +2456,6 @@ fn keytip_rv_() { assert!(h.state().session.journal.is_empty(), "RV must not journal"); } -#[test] -fn keytip_rw_() { - let mut h = harness(blank()); - sequence(&mut h, "RW", false); - assert!(h.state().dialog.is_none(), "RW must not open a dialog"); - assert!(h.state().message.is_none(), "RW must not open a message box"); - assert!(h.state().toast.is_none(), "RW must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "RW must not journal"); -} - #[test] fn keytip_wa_() { let mut h = harness(blank()); @@ -2533,16 +2463,6 @@ fn keytip_wa_() { assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.arrangeAll"), "WA must journal view.arrangeAll"); } -#[test] -fn keytip_wb_() { - let mut h = harness(blank()); - sequence(&mut h, "WB", false); - assert!(h.state().dialog.is_none(), "WB must not open a dialog"); - assert!(h.state().message.is_none(), "WB must not open a message box"); - assert!(h.state().toast.is_none(), "WB must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "WB must not journal"); -} - #[test] fn keytip_wc_() { let mut h = harness(blank()); @@ -2712,16 +2632,6 @@ fn keytip_wvh_() { assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.headings"), "WVH must journal view.headings"); } -#[test] -fn keytip_wvl_() { - let mut h = harness(blank()); - sequence(&mut h, "WVL", false); - assert!(h.state().dialog.is_none(), "WVL must not open a dialog"); - assert!(h.state().message.is_none(), "WVL must not open a message box"); - assert!(h.state().toast.is_none(), "WVL must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "WVL must not journal"); -} - #[test] fn keytip_wvn_() { let mut h = harness(blank()); @@ -2739,16 +2649,6 @@ fn keytip_wvr_() { assert!(h.state().session.journal.iter().any(|(id, _)| id == "view.ruler"), "WVR must journal view.ruler"); } -#[test] -fn keytip_wvv_() { - let mut h = harness(blank()); - sequence(&mut h, "WVV", false); - assert!(h.state().dialog.is_none(), "WVV must not open a dialog"); - assert!(h.state().message.is_none(), "WVV must not open a message box"); - assert!(h.state().toast.is_none(), "WVV must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "WVV must not journal"); -} - #[test] fn keytip_ww_() { let mut h = harness(blank()); @@ -2805,16 +2705,6 @@ fn keytip_fc_() { assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.close"), "FC must journal file.close"); } -#[test] -fn keytip_fd_() { - let mut h = harness(blank()); - sequence(&mut h, "FD", false); - assert!(h.state().dialog.is_none(), "FD must not open a dialog"); - assert!(h.state().message.is_none(), "FD must not open a message box"); - assert!(h.state().toast.is_none(), "FD must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "FD must not journal"); -} - #[test] fn keytip_fea_() { let mut h = harness(blank()); @@ -2829,16 +2719,6 @@ fn keytip_fec_() { assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.saveAs"), "FEC must journal file.saveAs"); } -#[test] -fn keytip_fh_() { - let mut h = harness(blank()); - sequence(&mut h, "FH", false); - assert!(h.state().dialog.is_none(), "FH must not open a dialog"); - assert!(h.state().message.is_none(), "FH must not open a message box"); - assert!(h.state().toast.is_none(), "FH must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "FH must not journal"); -} - #[test] fn keytip_fi_() { let mut h = harness(blank()); @@ -2867,16 +2747,6 @@ fn keytip_fp_() { assert!(h.state().session.journal.iter().any(|(id, _)| id == "file.print"), "FP must journal file.print"); } -#[test] -fn keytip_ft_() { - let mut h = harness(blank()); - sequence(&mut h, "FT", false); - assert!(h.state().dialog.is_none(), "FT must not open a dialog"); - assert!(h.state().message.is_none(), "FT must not open a message box"); - assert!(h.state().toast.is_none(), "FT must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "FT must not journal"); -} - #[test] fn keytip_q_() { let mut h = harness(blank()); @@ -2887,49 +2757,9 @@ fn keytip_q_() { assert!(h.state().session.journal.is_empty(), "Q must not journal"); } -#[test] -fn keytip_6_() { - let mut h = harness(blank()); - sequence(&mut h, "6", false); - assert!(h.state().dialog.is_none(), "6 must not open a dialog"); - assert!(h.state().message.is_none(), "6 must not open a message box"); - assert!(h.state().toast.is_none(), "6 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "6 must not journal"); -} - -#[test] -fn keytip_7_() { - let mut h = harness(blank()); - sequence(&mut h, "7", false); - assert!(h.state().dialog.is_none(), "7 must not open a dialog"); - assert!(h.state().message.is_none(), "7 must not open a message box"); - assert!(h.state().toast.is_none(), "7 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "7 must not journal"); -} - -#[test] -fn keytip_8_() { - let mut h = harness(blank()); - sequence(&mut h, "8", false); - assert!(h.state().dialog.is_none(), "8 must not open a dialog"); - assert!(h.state().message.is_none(), "8 must not open a message box"); - assert!(h.state().toast.is_none(), "8 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "8 must not journal"); -} - -#[test] -fn keytip_9_() { - let mut h = harness(blank()); - sequence(&mut h, "9", false); - assert!(h.state().dialog.is_none(), "9 must not open a dialog"); - assert!(h.state().message.is_none(), "9 must not open a message box"); - assert!(h.state().toast.is_none(), "9 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "9 must not journal"); -} - #[test] fn generated_test_count_matches_the_table() { - const EXPECTED: usize = 381; + const EXPECTED: usize = 364; assert_eq!(gridcraft_ui_egui::keytips::keytip_table::SEQUENCES.len(), EXPECTED); } From 9e9f9aec84710eb44759aa3886fc00fc4beccc09 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 23:22:36 +0300 Subject: [PATCH 14/15] T9: contributor credit and ui-parity docs for the keytip work --- contributors/people.toml | 4 ++++ docs/ui-parity.md | 2 ++ 2 files changed, 6 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index 681a4dd..e06e7ca 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -54,3 +54,7 @@ sync_github_name = true [people.GhostMitra] sync_github_name = true +[people.jsymes21-byte] +real_name = "James Symes" +sync_github_name = true + diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 697c2e1..73686c1 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -69,6 +69,7 @@ Sources: `crates/ui-egui/src` (ribbon, grid, editor, formula bar, dialogs, panes | 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 | | Requests | — | #110 (more shortcuts and hot keys), #33 (sequential Alt; initial support landed in #43) | ### Shortcut audit (#110) @@ -162,5 +163,6 @@ scrollbar bugs 2–4 · pixel/feel pass against observed Excel 7–12. | 2026-10-11 | minor | Keyboard shortcut audit against Microsoft's Windows and Mac lists; missing shortcuts for existing features added (#110) | | 2026-10-11 | minor | File tab Backstage view and File › Options (#174) | | 2026-10-11 | minor | Selection outline glide and the Animations setting | +| 2026-10-11 | major | Sequential Alt keytips: full-tab table (364 sequences), badges on every control, dropdowns open for typing/arrow editing, colour-palette arrows, real-Excel corrections (issues #33, #49) | | 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 | From 7a2361de7f455a2afde4ff3a4863ac6b9e65b169 Mon Sep 17 00:00:00 2001 From: James Date: Sun, 11 Oct 2026 23:54:15 +0300 Subject: [PATCH 15/15] T10: resolve conflicts with main; new upstream commands go live (slicer, pivot chart, forecast sheet, show comments) --- .../src/keytips/keytip_table/generated.rs | 70 +++++++++---------- crates/ui-egui/src/keytips/keytips.json | 16 ++--- crates/ui-egui/tests/keytips.rs | 13 ++-- crates/ui-egui/tests/keytips_generated.rs | 32 ++------- 4 files changed, 57 insertions(+), 74 deletions(-) diff --git a/crates/ui-egui/src/keytips/keytip_table/generated.rs b/crates/ui-egui/src/keytips/keytip_table/generated.rs index 100a20f..143c157 100644 --- a/crates/ui-egui/src/keytips/keytip_table/generated.rs +++ b/crates/ui-egui/src/keytips/keytip_table/generated.rs @@ -180,11 +180,11 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("NSA", Entry::Finish(Action::Control("insert.chart.statistic"))), // Excel: Statistical Chart; Statistical chart (kind TBD) ("NSC", Entry::Finish(Action::Pending("insert.screenshot"))), // pending; Excel: Screenshot Menu; Screenshot; reserved until the command lands ("NSD", Entry::Finish(Action::Command("insert.chart"))), // Excel: Combo Chart; Combo (kind TBD) - ("NSF", Entry::Finish(Action::Pending("insert.slicer"))), // pending; Excel: Slicer; insert.slicer missing (gap #8); reserved until the command lands - ("NSH", Entry::Finish(Action::Command("insert.shape"))), // Excel: Shapes + ("NSF", Entry::Finish(Action::Command("insert.slicer"))), // Excel: Slicer; insert.slicer missing (gap #8); reserved until the command lands + ("NSH", Entry::Finish(Action::Command("insert.shape"))), // Excel: Shapes ("NSP", Entry::Finish(Action::Command("insert.recommendedPivotTables"))), // Excel: Recommended PivotTables ("NST", Entry::Finish(Action::Pending("insert.timeline"))), // pending; Excel: Timeline; insert.timeline missing (gap #8); reserved until the command lands - ("NSZ", Entry::Finish(Action::Pending("insert.pivotChart"))), // pending; Excel: PivotChart; PivotChart; reserved until the command lands + ("NSZ", Entry::Finish(Action::Command("insert.pivotChart"))), // Excel: PivotChart; PivotChart; reserved until the command lands ("NT", Entry::Finish(Action::Command("insert.table"))), // Excel: Table ("NU", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Symbols; containment(Symbol) ("NZS", Entry::Finish(Action::Command("insert.symbol"))), // Excel: Excel: Symbol (modern) @@ -265,7 +265,7 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("AF1C", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from Clipboard (source TBD) ("AF1P", Entry::Finish(Action::Command("insert.picture"))), // Excel: Picture from File (source TBD) ("AFA", Entry::Finish(Action::Pending("data.fromAccess"))), // pending; Excel: Get External Data: From Access; gap #12 Get & Transform; reserved until the command lands - ("AFC", Entry::Finish(Action::Pending("data.forecastSheet"))), // pending; Excel: Forecast Sheet; IN FLIGHT: vangel76 PR #325; reserved until the command lands + ("AFC", Entry::Finish(Action::Command("data.forecastSheet"))), // Excel: Forecast Sheet; IN FLIGHT: vangel76 PR #325; reserved until the command lands ("AFE", Entry::Finish(Action::Pending("data.analyzeData"))), // pending; Excel: Analyze Data; AI feature GAP; reserved until the command lands ("AFF", Entry::Finish(Action::Command("edit.flashFill"))), // Excel: Flash Fill ("AFO", Entry::Finish(Action::Pending("data.fromOtherSources"))), // pending; Excel: Get External Data: From Other Sources; gap #12; reserved until the command lands @@ -316,50 +316,50 @@ pub const SEQUENCES: &[(&str, Entry)] = &[ ("AX", Entry::Finish(Action::Pending("data.existingConnections"))), // pending; Excel: Existing Connections; gap #12; reserved until the command lands ("AY", Entry::Finish(Action::Command("data.reapply"))), // Excel: Reapply Filter; containment(Reapply) ("RA1", Entry::Finish(Action::Dialog("accessibility"))), // Excel: Check Accessibility - ("RA2", Entry::Finish(Action::Pending("review.showComments"))), // pending; Excel: Show All Comments; Show All Comments; reserved until the command lands + ("RA2", Entry::Finish(Action::Command("review.showComments"))), // Excel: Show All Comments; Show All Comments; reserved until the command lands ("RC", Entry::Finish(Action::Dialog("comment"))), // Excel: New Comment ("RD", Entry::Finish(Action::Command("review.deleteComment"))), // Excel: Delete Comment ("RG", Entry::Finish(Action::Pending("review.showChanges"))), // pending; Excel: Track Changes; reserved until the command lands ("RL", Entry::Finish(Action::Pending("review.translate"))), // pending; Excel: Translate; feature gap; reserved until the command lands - ("RN", Entry::Finish(Action::Pending("review.nextComment"))), // pending; Excel: Next Comment; Next Comment; reserved until the command lands + ("RN", Entry::Finish(Action::Command("review.nextComment"))), // Excel: Next Comment ("RO", Entry::Finish(Action::Command("review.protectWorkbook"))), // Excel: Protect and Share Workbook ("RPS", Entry::Finish(Action::Dialog("protectSheet"))), // Excel: Protect Sheet ("RPW", Entry::Finish(Action::Command("review.protectWorkbook"))), // Excel: Protect Workbook ("RRS", Entry::Finish(Action::Pending("review.smartLookup"))), // pending; Excel: Smart Lookup; feature gap; reserved until the command lands ("RS", Entry::Finish(Action::Dialog("spelling"))), // Excel: Spelling ("RTN", Entry::Finish(Action::Command("review.newNote"))), // Excel: New Note - ("RV", Entry::Finish(Action::Pending("review.previousComment"))), // pending; Excel: Previous Comment; Previous Comment; reserved until the command lands - ("WA", Entry::Finish(Action::Command("view.arrangeAll"))), // Excel: Arrange All - ("WC", Entry::Finish(Action::Command("view.customViews"))), // Excel: Custom Views - ("WF", Entry::Continue), // Excel: Freeze Panes Menu; group level: + ("RV", Entry::Finish(Action::Command("review.previousComment"))), // Excel: Previous Comment + ("WA", Entry::Finish(Action::Command("view.arrangeAll"))), // Excel: Arrange All + ("WC", Entry::Finish(Action::Command("view.customViews"))), // Excel: Custom Views + ("WF", Entry::Continue), // Excel: Freeze Panes Menu; group level: ("WFC", Entry::Finish(Action::Command("view.freezeFirstColumn"))), // Excel: Freeze First Column - ("WFF", Entry::Finish(Action::Command("view.freezePanes"))), // Excel: Freeze Panes - ("WFR", Entry::Finish(Action::Command("view.freezeTopRow"))), // Excel: Freeze Top Row - ("WG", Entry::Finish(Action::Command("view.zoomToSelection"))), // Excel: Zoom To Selection - ("WH", Entry::Finish(Action::Command("view.hideWindow"))), // Excel: Hide Window - ("WI", Entry::Finish(Action::Command("view.pageBreakPreview"))), // Excel: Page Break Preview - ("WJ", Entry::Finish(Action::Control("view.zoom"))), // Excel: Zoom To 100%; runs the 100% zoom handler (percent param) - ("WL", Entry::Finish(Action::Command("view.normal"))), // Excel: Normal View; containment(Normal) - ("WM", Entry::Continue), // Excel: Macros Menu; group level: containment(Macros) - ("WMR", Entry::Finish(Action::Command("view.macros"))), // Excel: Record Macro; Record Macro (macros menu) - ("WMV", Entry::Finish(Action::Command("view.macros"))), // Excel: View Macros; containment(Macros) - ("WN", Entry::Finish(Action::Command("view.newWindow"))), // Excel: New Window - ("WP", Entry::Finish(Action::Command("view.pageLayout"))), // Excel: Page Layout View - ("WQ", Entry::Finish(Action::Dialog("zoom"))), // Excel: Zoom; opens the Zoom dialog - ("WR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler - ("WS", Entry::Finish(Action::Command("view.split"))), // Excel: Split + ("WFF", Entry::Finish(Action::Command("view.freezePanes"))), // Excel: Freeze Panes + ("WFR", Entry::Finish(Action::Command("view.freezeTopRow"))), // Excel: Freeze Top Row + ("WG", Entry::Finish(Action::Command("view.zoomToSelection"))), // Excel: Zoom To Selection + ("WH", Entry::Finish(Action::Command("view.hideWindow"))), // Excel: Hide Window + ("WI", Entry::Finish(Action::Command("view.pageBreakPreview"))), // Excel: Page Break Preview + ("WJ", Entry::Finish(Action::Control("view.zoom"))), // Excel: Zoom To 100%; runs the 100% zoom handler (percent param) + ("WL", Entry::Finish(Action::Command("view.normal"))), // Excel: Normal View; containment(Normal) + ("WM", Entry::Continue), // Excel: Macros Menu; group level: containment(Macros) + ("WMR", Entry::Finish(Action::Command("view.macros"))), // Excel: Record Macro; Record Macro (macros menu) + ("WMV", Entry::Finish(Action::Command("view.macros"))), // Excel: View Macros; containment(Macros) + ("WN", Entry::Finish(Action::Command("view.newWindow"))), // Excel: New Window + ("WP", Entry::Finish(Action::Command("view.pageLayout"))), // Excel: Page Layout View + ("WQ", Entry::Finish(Action::Dialog("zoom"))), // Excel: Zoom; opens the Zoom dialog + ("WR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler + ("WS", Entry::Finish(Action::Command("view.split"))), // Excel: Split ("WT", Entry::Finish(Action::Pending("view.resetWindowPosition"))), // pending; Excel: Reset Window Position; reserved until the command lands - ("WU", Entry::Finish(Action::Command("view.unhideWindow"))), // Excel: Unhide Window - ("WVC", Entry::Finish(Action::Pending("view.focusCell"))), // pending; Excel: Focus Cell Menu; Focus Cell; reserved until the command lands - ("WVF", Entry::Finish(Action::Control("view.formulaBar"))), // Excel: Toggle Formula Bar; toggles the formula bar - ("WVH", Entry::Finish(Action::Command("view.headings"))), // Excel: Toggle Headings; containment(Headings) - ("WVN", Entry::Finish(Action::Pending("view.navigation"))), // pending; Excel: Navigation; Navigation pane; reserved until the command lands - ("WVR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler - ("WW", Entry::Finish(Action::Command("view.switchWindows"))), // Excel: Switch Windows + ("WU", Entry::Finish(Action::Command("view.unhideWindow"))), // Excel: Unhide Window + ("WVC", Entry::Finish(Action::Pending("view.focusCell"))), // pending; Excel: Focus Cell Menu; Focus Cell; reserved until the command lands + ("WVF", Entry::Finish(Action::Control("view.formulaBar"))), // Excel: Toggle Formula Bar; toggles the formula bar + ("WVH", Entry::Finish(Action::Command("view.headings"))), // Excel: Toggle Headings; containment(Headings) + ("WVN", Entry::Finish(Action::Pending("view.navigation"))), // pending; Excel: Navigation; Navigation pane; reserved until the command lands + ("WVR", Entry::Finish(Action::Command("view.ruler"))), // Excel: Ruler + ("WW", Entry::Finish(Action::Command("view.switchWindows"))), // Excel: Switch Windows ("JAM", Entry::Finish(Action::Command("chart.formatSelection"))), // Excel: Chart format: Format Selection pane; containment(Format Selection) - ("JCA", Entry::Finish(Action::Command("chart.addElement"))), // Excel: Chart: Add Chart Element; containment(Add Chart Element) - ("JCC", Entry::Finish(Action::Command("chart.changeType"))), // Excel: Chart: Change Chart Type; containment(Change Chart Type) - ("JTS", Entry::Finish(Action::Command("table.style"))), // Excel: Table: table style + ("JCA", Entry::Finish(Action::Command("chart.addElement"))), // Excel: Chart: Add Chart Element; containment(Add Chart Element) + ("JCC", Entry::Finish(Action::Command("chart.changeType"))), // Excel: Chart: Change Chart Type; containment(Change Chart Type) + ("JTS", Entry::Finish(Action::Command("table.style"))), // Excel: Table: table style ("JTV", Entry::Finish(Action::Command("table.summarizePivot"))), // Excel: Table: Summarize with PivotTable; containment(Summarize with PivotTable) ("FA", Entry::Finish(Action::Command("file.saveAs"))), // Excel: Save / Save As page; fires immediately in Excel; deeper aliases dropped as unreachable ("FC", Entry::Finish(Action::Command("file.close"))), // Excel: Close workbook diff --git a/crates/ui-egui/src/keytips/keytips.json b/crates/ui-egui/src/keytips/keytips.json index 18f41c9..cbb8e04 100644 --- a/crates/ui-egui/src/keytips/keytips.json +++ b/crates/ui-egui/src/keytips/keytips.json @@ -1188,7 +1188,7 @@ "seq": "NSF", "kind": "command", "target": "insert.slicer", - "pending": true, + "pending": false, "excel": "Slicer", "note": "insert.slicer missing (gap #8); reserved until the command lands" }, @@ -1218,7 +1218,7 @@ "seq": "NSZ", "kind": "command", "target": "insert.pivotChart", - "pending": true, + "pending": false, "excel": "PivotChart", "note": "PivotChart; reserved until the command lands" }, @@ -1773,7 +1773,7 @@ "seq": "AFC", "kind": "command", "target": "data.forecastSheet", - "pending": true, + "pending": false, "excel": "Forecast Sheet", "note": "IN FLIGHT: vangel76 PR #325; reserved until the command lands" }, @@ -2142,7 +2142,7 @@ "seq": "RA2", "kind": "command", "target": "review.showComments", - "pending": true, + "pending": false, "excel": "Show All Comments", "note": "Show All Comments; reserved until the command lands" }, @@ -2180,9 +2180,9 @@ "seq": "RN", "kind": "command", "target": "review.nextComment", - "pending": true, + "pending": false, "excel": "Next Comment", - "note": "Next Comment; reserved until the command lands" + "note": "" }, { "seq": "RO", @@ -2230,9 +2230,9 @@ "seq": "RV", "kind": "command", "target": "review.previousComment", - "pending": true, + "pending": false, "excel": "Previous Comment", - "note": "Previous Comment; reserved until the command lands" + "note": "" }, { "seq": "WA", diff --git a/crates/ui-egui/tests/keytips.rs b/crates/ui-egui/tests/keytips.rs index b526782..24be742 100644 --- a/crates/ui-egui/tests/keytips.rs +++ b/crates/ui-egui/tests/keytips.rs @@ -308,6 +308,7 @@ fn tab_letters_switch_to_their_ribbon_tabs() { assert_eq!(h.state().keytips.prefix(), None); } // A terminal tab unit with no deeper sequences finishes and clears. + key(&mut h, Key::F10); // re-arm letter(&mut h, Key::J, false); assert_eq!(h.state().keytips.prefix(), Some("J"), "J waits for its multi-letter unit"); letter(&mut h, Key::I, false); @@ -406,7 +407,7 @@ fn vertical_alignment_keytips_run_their_commands() { fn pending_sequences_are_silent_no_ops() { let mut h = harness(blank()); let journal = h.state().session.journal.len(); - sequence(&mut h, &[Key::N, Key::E], false); // NE -> insert.equation (missing) + sequence(&mut h, &[Key::N, Key::W], false); // NW -> insert.wordArt (missing) assert!(h.state().dialog.is_none(), "no dialog for a pending sequence"); assert!(h.state().message.is_none(), "no message box for a pending sequence"); assert!(h.state().toast.is_none(), "no toast for a pending sequence"); @@ -418,15 +419,15 @@ fn pending_sequences_are_silent_no_ops() { fn pending_sequences_go_live_when_the_command_lands() { // The command lands in the catalog (here: a test-registered stand-in). // The sequence must run it with no table edit. - gridcraft_engine::register_test_command("insert.slicer", |s, _| { + gridcraft_engine::register_test_command("insert.equation", |s, _| { s.execute("cell.set", json!({"cell": "Z9", "input": "live"}))?; Ok(json!({})) }); let mut h = harness(blank()); - sequence(&mut h, &[Key::N, Key::S, Key::F], false); // NSF -> insert.slicer + sequence(&mut h, &[Key::N, Key::E], false); // NE -> insert.equation assert_eq!(value(&h, "Z9"), Value::from("live"), "the pending sequence ran the landed command"); assert_eq!( - h.state().session.journal.iter().filter(|(id, _)| id == "insert.slicer").count(), + h.state().session.journal.iter().filter(|(id, _)| id == "insert.equation").count(), 1, "the landed command is journaled like any live command" ); @@ -445,9 +446,9 @@ fn control_keytips_open_dropdowns_and_run_handlers() { // WVF toggles the formula bar: pure UI state, no engine command exists. let mut h = harness(blank()); - assert!(h.state().ui.formula_bar); + assert!(h.state().session.prefs.formula_bar); sequence(&mut h, &[Key::W, Key::V, Key::F], false); - assert!(!h.state().ui.formula_bar, "WVF must toggle the formula bar off"); + assert!(!h.state().session.prefs.formula_bar, "WVF must toggle the formula bar off"); // WJ runs the 100% handler with its percent param ({} would raise "missing percent"). let mut h = harness(blank()); diff --git a/crates/ui-egui/tests/keytips_generated.rs b/crates/ui-egui/tests/keytips_generated.rs index dd71255..50c5731 100644 --- a/crates/ui-egui/tests/keytips_generated.rs +++ b/crates/ui-egui/tests/keytips_generated.rs @@ -1276,7 +1276,7 @@ fn keytip_nq_() { fn keytip_nr_() { let mut h = harness(blank()); sequence(&mut h, "NR", false); - assert!(h.state().session.journal.iter().any(|(id, _)| id == "insert.recommendedCharts"), "NR must journal insert.recommendedCharts"); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NR must invoke insert.recommendedCharts and surface its missing-params error"); } #[test] @@ -1308,10 +1308,7 @@ fn keytip_nsd_() { fn keytip_nsf_() { let mut h = harness(blank()); sequence(&mut h, "NSF", false); - assert!(h.state().dialog.is_none(), "NSF must not open a dialog"); - assert!(h.state().message.is_none(), "NSF must not open a message box"); - assert!(h.state().toast.is_none(), "NSF must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "NSF must not journal"); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NSF must invoke insert.slicer and surface its missing-params error"); } #[test] @@ -1345,10 +1342,7 @@ fn keytip_nst_() { fn keytip_nsz_() { let mut h = harness(blank()); sequence(&mut h, "NSZ", false); - assert!(h.state().dialog.is_none(), "NSZ must not open a dialog"); - assert!(h.state().message.is_none(), "NSZ must not open a message box"); - assert!(h.state().toast.is_none(), "NSZ must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "NSZ must not journal"); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "NSZ must invoke insert.pivotChart and surface its missing-params error"); } #[test] @@ -1934,10 +1928,7 @@ fn keytip_afa_() { fn keytip_afc_() { let mut h = harness(blank()); sequence(&mut h, "AFC", false); - assert!(h.state().dialog.is_none(), "AFC must not open a dialog"); - assert!(h.state().message.is_none(), "AFC must not open a message box"); - assert!(h.state().toast.is_none(), "AFC must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "AFC must not journal"); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "AFC must invoke data.forecastSheet and surface its missing-params error"); } #[test] @@ -2351,10 +2342,7 @@ fn keytip_ra1_() { fn keytip_ra2_() { let mut h = harness(blank()); sequence(&mut h, "RA2", false); - assert!(h.state().dialog.is_none(), "RA2 must not open a dialog"); - assert!(h.state().message.is_none(), "RA2 must not open a message box"); - assert!(h.state().toast.is_none(), "RA2 must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "RA2 must not journal"); + assert!(h.state().message.is_none() && h.state().toast.is_none(), "RA2 must run review.showComments cleanly"); } #[test] @@ -2395,10 +2383,7 @@ fn keytip_rl_() { fn keytip_rn_() { let mut h = harness(blank()); sequence(&mut h, "RN", false); - assert!(h.state().dialog.is_none(), "RN must not open a dialog"); - assert!(h.state().message.is_none(), "RN must not open a message box"); - assert!(h.state().toast.is_none(), "RN must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "RN must not journal"); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "RN must invoke review.nextComment and surface its missing-params error"); } #[test] @@ -2450,10 +2435,7 @@ fn keytip_rtn_() { fn keytip_rv_() { let mut h = harness(blank()); sequence(&mut h, "RV", false); - assert!(h.state().dialog.is_none(), "RV must not open a dialog"); - assert!(h.state().message.is_none(), "RV must not open a message box"); - assert!(h.state().toast.is_none(), "RV must not raise a toast"); - assert!(h.state().session.journal.is_empty(), "RV must not journal"); + assert!(h.state().message.is_some() || h.state().toast.is_some(), "RV must invoke review.previousComment and surface its missing-params error"); } #[test]