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,