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.
This commit is contained in:
James committed 2026-10-11 23:40:23 +03:00
1 parent 7591fca086
commit cbd9bb51c6
9 files changed
+257 -408

No files matched your search

+4
View File
@@ -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)]
+2 -2
View File
@@ -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"));
}
@@ -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
+51 -156
View File
@@ -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",
+12
View File
@@ -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<String>,
/// 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),
}
}
}
+71 -48
View File
@@ -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<egui::SetOpenCommand> {
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(&current)).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(&current)).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<String> = 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);
+23
View File
@@ -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;
+35 -133
View File
@@ -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);
}
+8 -3
View File
@@ -138,7 +138,7 @@ fn parse_and_probe(source: &str) -> Result<(Vec<Entry>, 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<Entry>, 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,