mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-30 13:27:27 -06:00
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:
co-authored by
Brandon Thomas
parent
9f7330664b
commit
b309af4148
@@ -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 |
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user