Keytips: Alt, J, T and Alt, J, L open the table tabs like Word (#1185)

Table Design and Table Layout were in the keytip tab list inside a table but had no letter, so no badge showed and no key reached them. They now take Word's two-letter badges, JT and JL: J waits for the second letter and narrows the tab badges to the J ones, then the tab's control badges show as on any other tab.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Marco Arnulfo
2026-10-14 17:58:18 -04:00
committed by GitHub
co-authored by Brandon Thomas
parent 9f7330664b
commit b309af4148
4 changed files with 57 additions and 14 deletions
+1
View File
@@ -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 |
+53 -12
View File
@@ -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();
+2 -2
View File
@@ -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));
+1
View File
@@ -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 |