diff --git a/ROADMAP.md b/ROADMAP.md index 0a06d3afc..73b3c30ea 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -214,6 +214,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-12 | Alt+Q moves to "Tell me" command search, as in Word for Windows (#819); not on macOS, where Option+Q types a character | | 2026-10-12 | PDF export of long documents with many internal links (a linked table of contents) is linear: NIH's 67-page protocol template 10.6 s → 0.9 s | | 2026-10-12 | Keytips: Word for Windows' tab letters (File F, Design G, References S, Review R, View W, Help Y) | +| 2026-10-13 | Keytips reach the table tabs as in Word: Alt, J, T for Table Design and Alt, J, L for Table Layout, then their controls' badges (#1183) | | 2026-10-12 | Table-of-contents links look like Word's: plain text, not blue and underlined (Word ignores the Hyperlink style on TOC entry links; measured in 9 cases) | | 2026-10-12 | Line heights like Word's: spaces and tabs never make a line taller (13 cases measured in Word for Windows); tables of contents with big tab runs were a third taller | | 2026-10-12 | `.doc` import: tab stops (centred titles, right-aligned dates, leaders) and sections ending at their section marks (continuous breaks, footers); EPA .doc templates now paginate like Word | diff --git a/crates/ui-egui/src/keytips.rs b/crates/ui-egui/src/keytips.rs index 6eae7283f..45c8a1c9e 100644 --- a/crates/ui-egui/src/keytips.rs +++ b/crates/ui-egui/src/keytips.rs @@ -81,12 +81,15 @@ const TAB_LETTERS: [(&str, &str); 11] = [ ("Help", "Y"), ]; +/// Word's contextual table tabs take two letters (Alt, J, T and Alt, J, L), shown in a table. +const TABLE_TAB_LETTERS: [(&str, &str); 2] = [("Table Design", "JT"), ("Table Layout", "JL")]; + /// The Search box's badge beside the tab badges (Word's Alt, Q). pub const SEARCH_LETTER: &str = "Q"; /// The letter badge for a tab (empty when it has none). pub fn tab_letter(tab: &str) -> &'static str { - TAB_LETTERS.iter().find(|(t, _)| *t == tab).map(|(_, l)| *l).unwrap_or("") + TAB_LETTERS.iter().chain(&TABLE_TAB_LETTERS).find(|(t, _)| *t == tab).map(|(_, l)| *l).unwrap_or("") } /// The tabs that carry a badge, in display order. @@ -487,10 +490,11 @@ pub fn badge(ui: &Ui, rect: Rect, letter: &str) { p.text(r.center(), Align2::CENTER_CENTER, letter, font, t.on_accent); } -/// Record a tab badge over a tab's real rect, called as each tab is laid out. -pub fn record_tab(rect: Rect, tab: &str, out: &mut Vec<(Rect, String)>) { +/// Record a tab badge over a tab's real rect, called as each tab is laid out. After a typed +/// prefix (J, or Quick Access Toolbar digits) only the badges it can still complete show. +pub fn record_tab(rect: Rect, tab: &str, prefix: &str, out: &mut Vec<(Rect, String)>) { let l = tab_letter(tab); - if !l.is_empty() { + if !l.is_empty() && l.starts_with(prefix) { out.push((rect, l.to_string())); } } @@ -528,18 +532,21 @@ pub fn handle_key(app: &mut WordApp, key: egui::Key) -> bool { app.ui.keytip_prefix.clear(); return true; }; - if app.ui.keytips == Phase::Tabs && (name.chars().all(|c| c.is_ascii_digit()) || !app.ui.keytip_prefix.is_empty()) { + // Quick Access Toolbar badges are all digits; a pending prefix keeps its kind. + let typed = if app.ui.keytip_prefix.is_empty() { name.as_str() } else { app.ui.keytip_prefix.as_str() }; + if app.ui.keytips == Phase::Tabs && typed.chars().all(|c| c.is_ascii_digit()) { qat_key(app, &name); return true; } match app.ui.keytips { Phase::Tabs => { - let mut hit: Option<&'static str> = None; - for tab in badge_tabs(app) { - if tab_letter(tab) == name { - hit = Some(tab); - break; - } + let name = format!("{}{name}", app.ui.keytip_prefix); + let tabs = badge_tabs(app); + let hit = tabs.iter().copied().find(|t| tab_letter(t) == name); + // J waits for the table tab's second letter. + if hit.is_none() && tabs.iter().any(|t| tab_letter(t).starts_with(&name)) { + app.ui.keytip_prefix = name; + return true; } match hit { Some(tab) => { @@ -869,6 +876,8 @@ mod tests { assert_eq!(tab_letter("Review"), "R"); assert_eq!(tab_letter("View"), "W"); assert_eq!(tab_letter("Help"), "Y"); + assert_eq!(tab_letter("Table Design"), "JT"); + assert_eq!(tab_letter("Table Layout"), "JL"); } #[test] @@ -880,7 +889,7 @@ mod tests { let a = app(); let catalog: Vec<&str> = wordcraft_engine::catalog::entries().into_iter().map(|e| e.3).collect(); let mut missing = Vec::new(); - for (tab, _) in TAB_LETTERS { + for (tab, _) in TAB_LETTERS.iter().chain(&TABLE_TAB_LETTERS) { for ctrl in controls(tab) { if let Target::Cmd { id, .. } = ctrl.target && a.session.registry.get(id).is_none() @@ -1005,6 +1014,38 @@ mod tests { assert_eq!(a.ui.tab, "Review"); } + /// Word's contextual table tabs take two letters (#1183): Alt, J, T is Table Design and + /// Alt, J, L is Table Layout. J alone waits for the second letter. + #[test] + fn alt_j_t_and_alt_j_l_open_the_table_tabs() { + let mut a = app(); + a.run("insert.table", serde_json::json!({"rows": 2, "cols": 2})).unwrap(); + assert!(crate::ribbon::in_table_public(&a)); + for (second, tab) in [(egui::Key::T, "Table Design"), (egui::Key::L, "Table Layout")] { + a.run("ui.tab", serde_json::json!({"tab": "Home"})).unwrap(); + activate(&mut a); + assert!(handle_key(&mut a, egui::Key::J)); + assert_eq!((a.ui.tab.as_str(), a.ui.keytips), ("Home", Phase::Tabs), "J waits for the second letter"); + assert!(handle_key(&mut a, second)); + assert_eq!((a.ui.tab.as_str(), a.ui.keytips), (tab, Phase::Commands)); + assert!(a.ui.keytip_prefix.is_empty()); + assert!(!letters_for(&a, tab).is_empty(), "{tab} badges its controls"); + a.ui.keytips = Phase::Off; + } + // J then a letter no table tab uses ends keytips without switching tabs. + a.run("ui.tab", serde_json::json!({"tab": "Home"})).unwrap(); + activate(&mut a); + handle_key(&mut a, egui::Key::J); + handle_key(&mut a, egui::Key::X); + assert_eq!((a.ui.tab.as_str(), a.ui.keytips), ("Home", Phase::Off)); + assert!(a.ui.keytip_prefix.is_empty()); + // Outside a table there is no J tab. + let mut b = app(); + activate(&mut b); + handle_key(&mut b, egui::Key::J); + assert_eq!(b.ui.keytips, Phase::Off); + } + #[test] fn qat_keytips_follow_custom_order_and_skip_disabled_commands() { let mut a = app(); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 74d3fc185..c0710c642 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -282,8 +282,8 @@ pub fn show(app: &mut WordApp, ui: &mut Ui) { let shown = if custom { tab } else { tl!(tab) }; let w = ui.ctx().fonts_mut(|f| f.layout_no_wrap(shown.to_string(), medium(12.5), t.text).size().x) + 18.0; let (r, resp) = ui.allocate_exact_size(vec2(w, 30.0), Sense::click()); - if app.ui.keytips == crate::keytips::Phase::Tabs && app.ui.keytip_prefix.is_empty() { - crate::keytips::record_tab(r, tab, &mut app.keytip_rects); + if app.ui.keytips == crate::keytips::Phase::Tabs { + crate::keytips::record_tab(r, tab, &app.ui.keytip_prefix, &mut app.keytip_rects); } let active = app.ui.tab == tab && !app.ui.backstage; resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, true, active, shown)); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 7481a34f2..fd7863312 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -88,6 +88,7 @@ Document title opens Rename Document; the account badge opens Options › Genera | 2026-10-11 | trivial | Quick Access Toolbar KeyTips (#620): 1–9, zero-prefixed numbers for longer toolbars, disabled commands stay disabled, Escape cancels, badge keystrokes never type into the document; persisted File › Options switch | | 2026-10-11 | trivial | Ribbon buttons that run a command (alignment, Bold, styles…) hand keyboard focus back to the document, so typing continues without another click; they also report their name and enabled state to screen readers (#686) | | 2026-10-12 | trivial | Keytips use Word for Windows' tab letters, as observed there: File F (opens the Backstage), Design G, References S, Review R, View W, Help Y (were L, R, S, V and none) | +| 2026-10-13 | trivial | Keytips reach the contextual table tabs with Word's two-letter badges: Alt, J, T opens Table Design and Alt, J, L Table Layout; after J only the J badges stay (#1183) | | 2026-10-12 | trivial | Shift+F10 opens the page's context menu below the caret, with the spelling suggestions and synonyms for the word there; `ui.contextMenu` for agents (#821). The Menu key itself isn't passed on by the windowing layer yet | | 2026-10-12 | trivial | Search: Alt+Q (and Alt, Q) opens the Search box from anywhere, as in Word; the key's own "q" isn't typed; every word of a query matches, so "insert table" finds Insert › Tables › Table; `tools.search` lists matches for agents (#819) | | 2026-10-12 | trivial | Find, Replace and Go To as in Word (#712): Ctrl+F and Home › Find open only the Navigation pane with its search box ready to type in (the selected text searched for); Find and Replace is one dialog with Find, Replace and Go To tabs (Advanced Find, Ctrl+H, Ctrl+G / F5); Go To moves to a page (a number, or +2 / −1 from the current page; Next and Previous step a page) or a bookmark |