Alt+Q opens Search like Word (#819) (#919)

* Search: Alt+Q opens it from anywhere, like Word (#819)

- Alt+Q (and Alt, then Q on the keytips) opens the Search box with its
  text field focused, from the page or anywhere else; the key's own "q"
  is never typed into the box or the document. The title-bar box shows
  its Q keytip and its shortcut in the tooltip.
- An empty box suggests common commands; every word of a query has to
  match (so "insert table" finds Insert > Tables > Table, as the box's
  own hint promises); Enter runs the top match; "Find ... in the
  document" opens the Navigation pane on the results. Matching is
  about 2x faster than before.
- tools.search lists matching commands for scripts and agents.

Observed in Word for Windows (Microsoft 365): Alt+Q and Alt, Q focus
Search with nothing typed; multi-word queries match commands; the list
also offers finding the words in the document.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Search: Bulgarian, Czech, German, French, Hungarian, Italian and Polish strings

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-12 10:38:53 -04:00 committed by GitHub
parent 8c8d5611a4
commit 03274b3b4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 293 additions and 29 deletions

View File

@ -164,6 +164,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-12 | Automatic paragraph spacing drops between list items, as in Word (16 cases measured in Word for Windows): bulleted items that ask for automatic space before and after now touch, as in NIH's monitoring plan |
| 2026-10-12 | Rendering like Word for Windows, found by comparing real .gov documents page by page: text is kerned only where Kerning for fonts asks for it (line breaks now match Word's), and automatic paragraph spacing (`w:beforeAutospacing`/`w:afterAutospacing`, common in pasted web text) is 14 pt, dropped at the document's and cells' edges |
| 2026-10-12 | Keyboard context menu (#821): Shift+F10 opens the page's right-click menu below the caret; `ui.contextMenu` |
| 2026-10-12 | Search (#819): Alt+Q, or Alt then Q, opens the Search box from anywhere with nothing typed into it; queries match every word; `tools.search` for agents |
| 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged |
| 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) |
|---|---|

View File

@ -35,6 +35,7 @@ Quick Access|Toolbar|Undo List|edit.undoTo
Quick Access|Toolbar|Redo|edit.redo
Quick Access|Toolbar|Repeat|edit.repeat
Quick Access|Toolbar|AutoSave|file.autosave
Title Bar|Search|Search (Tell Me)|tools.search
Home|Clipboard|Paste|edit.paste
Home|Clipboard|Paste Keep Text Only|edit.pasteText
Home|Clipboard|Paste Merge Formatting|edit.pasteMerge

View File

@ -378,6 +378,11 @@ pub fn specs() -> Vec<CommandSpec> {
r#"{"hide"?: tab, "show"?: tab, "newTab"?: {"name"?, "after"?: tab}, "newGroup"?: {"tab", "name"?}, "add"?: {"tab", "group", "command", "after"?: id}, "remove"?: {"tab", "group"?, "command"?}, "rename"?: {"tab", "group"?, "to"}, "move"?: {"tab", "group"?, "command"?, "by": int}, "qatAdd"?: id, "qatRemove"?: id, "qatMove"?: {"command", "by": int}, "reset"?: true | "ribbon" | "qat", "layout"?: object} → {tabs: [{name, custom?, hidden?, groups?: [{name, commands}]}], qat: [id]}; only custom tabs and groups can be renamed or removed (built-in tabs can be hidden and moved); no params opens the dialog"#,
)
.pure(),
// Word's Search box in the title bar ("Tell me what you want to do"), Alt+Q (#819).
CommandSpec::new("tools.search", "Search", "Title Bar", search_commands)
.key(super::non_mac("Alt+Q"))
.params(r#"{"query"?: string} → {matches: [{id, label, location, shortcut}]} (every word of the query in the label, location or id; at most 50); no query opens the Search box"#)
.pure(),
CommandSpec::new("review.compare", "Compare", "Review › Compare", compare).params(COMPARE_PARAMS),
CommandSpec::new("review.combine", "Combine", "Review › Compare", compare).params(COMPARE_PARAMS),
CommandSpec::new("file.accessibility", "Check Accessibility", "Review › Accessibility", accessibility).pure(),
@ -1413,6 +1418,31 @@ fn customize_keyboard(s: &mut Session, v: &Value) -> CmdResult {
Ok(Value::Object(out))
}
/// `tools.search`: the commands whose label, location or id hold every word of `query`, or,
/// with no query, the Search box (`ui_requests` → the UI's command search).
fn search_commands(s: &mut Session, v: &Value) -> CmdResult {
let Some(query) = p::str(v, "query") else {
s.ui_requests.push(json!({"open": "commands"}));
return Ok(json!({"matches": []}));
};
let words: Vec<String> = query.split_whitespace().map(str::to_lowercase).collect();
let matches: Vec<Value> = if words.is_empty() {
Vec::new()
} else {
s.registry
.all()
.iter()
.filter(|c| {
let hay = format!("{} {} {}", c.label, c.location, c.id).to_lowercase();
words.iter().all(|w| hay.contains(w.as_str()))
})
.take(50)
.map(|c| json!({"id": c.id, "label": c.label, "location": c.location, "shortcut": c.shortcut}))
.collect()
};
Ok(json!({"matches": matches}))
}
/// Tools › Customize Ribbon: read and change the ribbon's tabs, custom groups and the Quick Access
/// Toolbar ([`crate::RibbonLayout`]). Without params it opens the dialog; with any, it only does
/// what they ask (no dialog).

View File

@ -301,8 +301,15 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) {
ptrack,
t.text_dim,
);
if resp.clicked() {
let _ = app.run("ui.dialog", json!({"name": "commands"}));
if app.ui.keytips == crate::keytips::Phase::Tabs {
app.keytip_rects.push((r, crate::keytips::SEARCH_LETTER.to_string()));
}
let tip = match crate::widgets::shortcut_text(app, "tools.search") {
k if k.is_empty() => tl!("Search").to_string(),
k => format!("{} ({k})", tl!("Search")),
};
if resp.on_hover_text(tip).clicked() {
let _ = app.run("tools.search", json!({}));
}
right_start = ui.min_rect().min.x;
});

View File

@ -301,6 +301,10 @@ pub enum Dialog {
},
Commands {
query: String,
/// Opened by a shortcut that may also send its key's text (Alt+Q's "q", in the same frame
/// or the next): that text isn't typed into the box; the next key pressed ends this (#819).
#[serde(skip)]
swallow: bool,
},
/// Columns, Symbol and Field ([`crate::dialogs_insert`], #321).
Insert(Box<crate::dialogs_insert::InsertDialog>),
@ -1087,7 +1091,7 @@ impl Dialog {
let regions = table_regions(app, &id);
Dialog::TableStyle { id: Some(id), name, based_on, region: 0, basis: regions.clone(), regions }
}
"commands" => Dialog::Commands { query: String::new() },
"commands" => Dialog::Commands { query: String::new(), swallow: true },
"icons" => Dialog::Icons { query: String::new(), category: String::new(), selected: String::new() },
"tableProperties" => {
let form = Box::new(TableForm::read(app)?);
@ -2212,35 +2216,44 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
Dialog::Insert(f) => crate::dialogs_insert::body(app, ui, f),
Dialog::Refs(f) => crate::dialogs_refs::body(app, ui, f),
Dialog::Commands { query } => {
Dialog::Commands { query, swallow } => {
if *swallow {
ui.ctx().input_mut(|i| i.events.retain(|e| !swallow_shortcut_text(swallow, e)));
}
let r = ui.add(egui::TextEdit::singleline(query).hint_text(tl!("Type a command, e.g. \"insert table\"")).desired_width(380.0));
r.request_focus();
let q = query.to_lowercase();
let localized_q = crate::i18n::lowercase(query);
let reg = app.session.registry.clone();
let mut hits: Vec<&wordcraft_engine::CommandSpec> = reg
.all()
.iter()
.filter(|c| {
!q.is_empty()
&& (c.label.to_lowercase().contains(&q)
|| crate::i18n::lowercase(tl!(c.label)).contains(&localized_q)
|| c.id.to_lowercase().contains(&q)
|| c.location.to_lowercase().contains(&q)
|| crate::i18n::lowercase(&crate::i18n::location(c.location)).contains(&localized_q))
})
.collect();
// An empty box suggests a few common commands, as Word's Search does.
let mut hits: Vec<&wordcraft_engine::CommandSpec> = if query.trim().is_empty() {
ui.label(egui::RichText::new(tl!("Suggested")).font(semibold(12.5)));
SEARCH_SUGGESTIONS.iter().filter_map(|id| reg.get(id)).collect()
} else {
reg.all().iter().filter(|c| command_matches(c, query)).collect()
};
hits.truncate(14);
let mut close = false;
for c in hits {
let enter = ui.input(|i| i.key_pressed(egui::Key::Enter));
let mut run: Option<&str> = enter.then(|| hits.first().map(|c| c.id)).flatten();
for c in &hits {
let sc = crate::widgets::shortcut_text(app, c.id);
let label = format!("{} — {}", tl!(c.label), crate::i18n::location(c.location));
if ui.add(egui::Button::new(label).shortcut_text(sc).min_size(vec2(380.0, 0.0))).clicked() {
let _ = app.run(c.id, json!({}));
close = true;
run = Some(c.id);
}
}
close || ui.input(|i| i.key_pressed(egui::Key::Escape))
// Search also finds the words in the document (the Navigation pane's results).
let text = query.trim().to_string();
let mut find = false;
if !text.is_empty() {
ui.separator();
let label = crate::i18n::fmt(tl!("Find “{text}” in the document"), &[("text", &text)]);
find = ui.add(egui::Button::new(label).min_size(vec2(380.0, 0.0))).clicked() || (enter && hits.is_empty());
}
if let Some(id) = run {
let _ = app.run(id, json!({}));
} else if find {
find_in_navigation_pane(app, ui.ctx(), &text);
}
run.is_some() || find || ui.input(|i| i.key_pressed(egui::Key::Escape))
}
Dialog::Icons { query, category, selected } => icons_body(app, ui, query, category, selected),
Dialog::PasteSpecial { formats, choice, payload, note } => {
@ -2991,6 +3004,50 @@ fn describe(st: &Value) -> String {
parts.join(", ")
}
/// What Search lists before anything is typed: common commands (#819).
const SEARCH_SUGGESTIONS: [&str; 3] = ["review.spelling", "review.wordCount", "file.print"];
/// Search's "Find … in the document": the Navigation pane opens on its results for `text`.
fn find_in_navigation_pane(app: &mut WordApp, ctx: &egui::Context, text: &str) {
ctx.data_mut(|d| d.insert_temp(egui::Id::new("nav_query"), text.to_string()));
app.ui.nav_tab = "results".into();
let _ = app.run("view.navigationPane", json!({"value": true}));
let _ = app.run("edit.find", json!({"text": text}));
}
/// Whether `e` is the text of the shortcut that opened a box (see `Dialog::Commands::swallow`):
/// a plain key press first means the user is typing, and one swallowed text is all there is.
fn swallow_shortcut_text(swallow: &mut bool, e: &egui::Event) -> bool {
match e {
egui::Event::Key { pressed: true, modifiers, .. } if !modifiers.alt => {
*swallow = false;
false
}
egui::Event::Text(_) if *swallow => {
*swallow = false;
true
}
_ => false,
}
}
/// Search's filter: every word of the query is in the command's label, location or id, in English
/// or in the interface language, so "insert table" finds Insert › Tables › Table (#819).
fn command_matches(c: &wordcraft_engine::CommandSpec, query: &str) -> bool {
if query.trim().is_empty() {
return false;
}
let hay = format!(
"{} {} {} {} {}",
c.label.to_lowercase(),
c.location.to_lowercase(),
c.id.to_lowercase(),
crate::i18n::lowercase(tl!(c.label)),
crate::i18n::lowercase(&crate::i18n::location(c.location))
);
query.split_whitespace().all(|w| hay.contains(&w.to_lowercase()) || hay.contains(&crate::i18n::lowercase(w)))
}
/// path data; Insert (or a double click) runs `insert.icon`. Returns true to close.
fn icons_body(app: &mut WordApp, ui: &mut Ui, query: &mut String, category: &mut String, selected: &mut String) -> bool {
use wordcraft_render::icon_lib::{self, Category};
@ -3113,6 +3170,40 @@ mod tests {
app.session.doc.para_at(&app.session.sel.focus).map(|p| p.props.clone()).unwrap_or_default()
}
/// #819: the text a shortcut sends with its key isn't typed into the box it opened, whether it
/// comes in the same frame or the next; real typing (a key press first) is.
#[test]
fn shortcut_text_is_swallowed_once() {
let key = |key, alt| egui::Event::Key {
key,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers { alt, ..Default::default() },
};
let text = |t: &str| egui::Event::Text(t.into());
let run = |events: &[egui::Event]| {
let mut swallow = true;
events.iter().filter(|e| !swallow_shortcut_text(&mut swallow, e)).cloned().collect::<Vec<_>>()
};
assert_eq!(run(&[key(egui::Key::Q, true), text("q")]), vec![key(egui::Key::Q, true)], "Alt+Q's q, same frame");
assert_eq!(run(&[text("q"), text("b")]), vec![text("b")], "the q a frame late; then typing");
assert_eq!(run(&[key(egui::Key::B, false), text("b")]), vec![key(egui::Key::B, false), text("b")], "a click-opened box takes typing");
}
/// #819: Search matches every word anywhere in the label, location or id, in any order.
#[test]
fn command_search_matches_every_word() {
let app = app_with("");
let reg = app.session.registry.clone();
let table = reg.get("insert.table").unwrap();
assert!(command_matches(table, "insert table"), "the box's own example");
assert!(command_matches(table, " Table INSERT "), "any order, any case, extra spaces");
assert!(!command_matches(table, "insert chart"), "every word has to match");
assert!(!command_matches(table, ""), "an empty box lists nothing");
assert!(!command_matches(table, " "));
}
/// Ordinary Turkish queries must find dotted-I labels, while canonical English labels and
/// ASCII command IDs retain their old case-insensitive search in every interface language.
#[test]
@ -3147,7 +3238,7 @@ mod tests {
("en", "INSERT.TABLE", "Table — Insert › Tables"),
] {
h.state_mut().ui.language = language.into();
h.state_mut().dialog = Some(Dialog::Commands { query: query.into() });
h.state_mut().dialog = Some(Dialog::Commands { query: query.into(), swallow: false });
h.run_steps(4);
assert!(
h.query(egui_kittest::kittest::By::new().role(egui::accesskit::Role::Button).label_contains(expected)).is_some(),

View File

@ -2462,3 +2462,5 @@
AutoFit Column by Double-Clicking Автопобиране на колона с двойно щракване
Insert Caption… Вмъкване на надпис…
Table Properties… Свойства на таблица…
Search Търсене
Title Bar Заглавна лента

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -2078,3 +2078,5 @@
Correct accidental usage of cAPS LOCK key Opravovat náhodné použití klávesy cAPS LOCK
Region: Oblast:
Automatic ({region}) Automaticky ({region})
Search Hledat
Title Bar Záhlaví okna

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -2072,3 +2072,5 @@
Correct accidental usage of cAPS LOCK key Unbeabsichtigtes Verwenden der fESTSTELLTASTE korrigieren
Region: Region:
Automatic ({region}) Automatisch ({region})
Search Suchen
Title Bar Titelleiste

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -167,6 +167,7 @@
Border Painter Pincel de bordes
Border\nPainter Pincel de\nbordes
Borders Bordes
Search Buscar
Show the mini toolbar when text is selected Mostrar la minibarra de herramientas al seleccionar texto
Spaces Espacios
Spelling Ortografía
@ -183,6 +184,7 @@
Page Border Borde de página
Apply to: Aplicar a:
These marks show even when Show/Hide ¶ is off. Estas marcas se muestran aunque Mostrar u ocultar ¶ esté desactivado.
Title Bar Barra de título
Typing replaces selected text Lo que escribes reemplaza el texto seleccionado
Update fields before printing Actualizar campos antes de imprimir
Use the Insert key to switch overtype on and off Usar la tecla Insert para activar y desactivar la sobrescritura

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -664,6 +664,7 @@
Save as Template Salvesta mallina
Save documents Dokumentide salvestamine
Saved Salvestatud
Search Otsi
Search Commands Otsi käske
Search commands and help Otsi käske ja abi
Search document Otsi dokumendist
@ -814,6 +815,7 @@
Tight Tihe
TOC Heading Sisukorra pealkiri
TOC {n} Sisukord {n}
Title Bar Tiitliriba
Top Border Ülemine ääris
Top of Page Lehe ülaserv
Top: Ülal:

Can't render this file because it has a wrong number of fields in line 12.

View File

@ -2081,3 +2081,5 @@
AutoFit Column by Double-Clicking Ajuster la colonne en double-cliquant
Insert Caption… Insérer une légende…
Table Properties… Propriétés du tableau…
Search Rechercher
Title Bar Barre de titre

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -2471,3 +2471,5 @@
AutoFit Column by Double-Clicking Oszlop automatikus igazítása dupla kattintással
Insert Caption… Felirat beszúrása…
Table Properties… Táblázat tulajdonságai…
Search Keresés
Title Bar Címsor

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -2457,3 +2457,5 @@ preview Title Esempio
Correct accidental usage of cAPS LOCK key Correggi l'uso accidentale del tasto bLOC mAIUSC
Region: Area geografica:
Automatic ({region}) Automatico ({region})
Search Cerca
Title Bar Barra del titolo

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -155,6 +155,7 @@
Right (vertical) 右 (縦書き)
Ruby text: ルビ文字:
Save documents ドキュメントの保存
Search 検索
Search for templates テンプレートを検索
Show all revisions inline すべての変更履歴を本文中に表示
Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. 表示 › モードの切り替えと同様に、文書の色を反転して表示します。保存、印刷、PDF は変わりません。
@ -198,6 +199,7 @@
Page Border ページ罫線の設定
Apply to: 適用先:
These marks show even when Show/Hide ¶ is off. これらの記号は、[編集記号の表示/非表示 ¶] がオフでも表示されます。
Title Bar タイトル バー
Typing replaces selected text 入力時に選択文字列を置き換える
Update fields before printing 印刷前にフィールドを更新する
Use the Insert key to switch overtype on and off Insert キーで上書きのオンとオフを切り替える

Can't render this file because it has a wrong number of fields in line 3.

View File

@ -2114,3 +2114,5 @@
Correct accidental usage of cAPS LOCK key Poprawiaj przypadkowe użycie klawisza cAPS LOCK
Region: Region:
Automatic ({region}) Automatycznie ({region})
Search Wyszukaj
Title Bar Pasek tytułu

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -162,6 +162,7 @@
Border Painter Pincel de bordas
Border\nPainter Pincel\nde bordas
Borders Bordas
Search Pesquisar
Show the mini toolbar when text is selected Mostrar a minibarra de ferramentas ao selecionar texto
Spaces Espaços
Spelling Ortografia
@ -178,6 +179,7 @@
Page Border Borda da página
Apply to: Aplicar a:
These marks show even when Show/Hide ¶ is off. Estas marcas aparecem mesmo com Mostrar/ocultar ¶ desativado.
Title Bar Barra de título
Typing replaces selected text O que você digita substitui o texto selecionado
Update fields before printing Atualizar campos antes de imprimir
Use the Insert key to switch overtype on and off Usar a tecla Insert para ativar e desativar a sobrescrita

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -189,6 +189,8 @@
Delete Style Удалить стиль
Delete Table Удалить таблицу
Design Дизайн
Search Поиск
Title Bar Строка заголовка
development build сборка для разработки
Diagonal Диагональ
Diamond Ромб

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -204,6 +204,7 @@
Preview Pregled
Remove Password Ukloni lozinku
Save documents Čuvanje dokumenata
Search Pretraga
Shadow, glow and soft edges Senka, sjaj i meke ivice
Shape Effects Efekti oblika
Show all revisions inline Prikaži sve izmene u tekstu
@ -255,6 +256,7 @@
Page Border Ivica strane
Apply to: Primeni na:
These marks show even when Show/Hide ¶ is off. Ove oznake se prikazuju i kada je Prikaži/sakrij ¶ isključeno.
Title Bar Naslovna traka
Typing replaces selected text Otkucano zamenjuje izabrani tekst
Update fields before printing Ažuriraj polja pre štampanja
Use the Insert key to switch overtype on and off Taster Insert uključuje i isključuje prepisivanje

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -208,6 +208,7 @@
Preview Преглед
Remove Password Уклони лозинку
Save documents Чување докумената
Search Претрага
Shadow, glow and soft edges Сенка, сјај и меке ивице
Shape Effects Ефекти облика
Show all revisions inline Прикажи све измене у тексту
@ -259,6 +260,7 @@
Page Border Ивица стране
Apply to: Примени на:
These marks show even when Show/Hide ¶ is off. Ове ознаке се приказују и када је Прикажи/сакриј ¶ искључено.
Title Bar Насловна трака
Typing replaces selected text Откуцано замењује изабрани текст
Update fields before printing Ажурирај поља пре штампања
Use the Insert key to switch overtype on and off Тастер Insert укључује и искључује преписивање

Can't render this file because it contains an unexpected character in line 11 and column 25.

View File

@ -622,6 +622,7 @@
Save As Farklı Kaydet
Save as Template Şablon Olarak Kaydet
Saved Kaydedildi
Search Ara
Search Commands Komut Ara
Search commands and help Komutlarda ve yardımda ara
Search document Belgede ara
@ -769,6 +770,7 @@ preview Title Örnek
TOA Heading Yetki Tablosu Başlığı
TOC Heading İçindekiler Başlığı
TOC {n} İçindekiler {n}
Title Bar Başlık Çubuğu
Top Border Üst Kenarlık
Top of Page Sayfa Üstü
Top: Üst:

Can't render this file because it has a wrong number of fields in line 15.

View File

@ -164,6 +164,7 @@
Border Painter Пензель меж
Border\nPainter Пензель\nмеж
Borders Межі
Search Пошук
Show the mini toolbar when text is selected Показувати міні-панель інструментів під час виділення тексту
Spaces Пробіли
Spelling Правопис
@ -180,6 +181,7 @@
Page Border Межа сторінки
Apply to: Застосувати до:
These marks show even when Show/Hide ¶ is off. Ці знаки відображаються, навіть коли «Показати/приховати ¶» вимкнено.
Title Bar Рядок заголовка
Typing replaces selected text Введення замінює виділений текст
Update fields before printing Оновлювати поля перед друком
Use the Insert key to switch overtype on and off Клавіша Insert вмикає та вимикає режим заміни

Can't render this file because it contains an unexpected character in line 6 and column 31.

View File

@ -167,6 +167,7 @@
Right (vertical) 右对齐(竖排)
Ruby text: 拼音文字:
Save documents 保存文档
Search 搜索
Search for templates 搜索模板
Show all revisions inline 以嵌入方式显示所有修订
Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. 像“视图 › 切换模式”一样以反转的颜色显示文档。保存、打印和 PDF 不受影响。
@ -210,6 +211,7 @@
Page Border 页面边框线
Apply to: 应用于:
These marks show even when Show/Hide ¶ is off. 即使关闭“显示/隐藏 ¶”,这些标记也会显示。
Title Bar 标题栏
Typing replaces selected text 键入内容替换所选文字
Update fields before printing 打印前更新域
Use the Insert key to switch overtype on and off 用 Insert 键切换改写模式

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -156,6 +156,7 @@
Right (vertical) 靠右(直書)
Ruby text: 注音文字:
Save documents 儲存文件
Search 搜尋
Search for templates 搜尋範本
Show all revisions inline 以內嵌方式顯示所有修訂
Show documents with their colours inverted, like View › Switch Modes. Saving, printing and PDFs are unchanged. 像「檢視 › 切換模式」一樣以反轉的色彩顯示文件。儲存、列印和 PDF 不受影響。
@ -199,6 +200,7 @@
Page Border 頁面框線設定
Apply to: 套用至:
These marks show even when Show/Hide ¶ is off. 即使關閉「顯示/隱藏 ¶」,這些標記也會顯示。
Title Bar 標題列
Typing replaces selected text 輸入的內容取代選取的文字
Update fields before printing 列印前更新功能變數
Use the Insert key to switch overtype on and off 使用 Insert 鍵切換取代模式

Can't render this file because it has a wrong number of fields in line 4.

View File

@ -376,6 +376,12 @@ pub fn canvas_events(app: &mut WordApp, ctx: &egui::Context) {
}
}
/// Whether a key runs its shortcut with the canvas unfocused (after a ribbon click, say): Mod and
/// Alt shortcuts (Alt+Q reaches Search from anywhere, as in Word, #819) and the function keys.
fn is_global_shortcut(key: Key, m: Modifiers) -> bool {
m.command || m.alt || is_function_key(key)
}
/// Command shortcuts when the canvas isn't focused but no text field is either.
pub fn global_shortcuts(app: &mut WordApp, ctx: &egui::Context) {
// Customize Keyboard records the keys pressed; they never run commands meanwhile.
@ -385,7 +391,7 @@ pub fn global_shortcuts(app: &mut WordApp, ctx: &egui::Context) {
let events = ctx.input(|i| i.events.clone());
for e in events {
if let egui::Event::Key { key, pressed: true, modifiers, .. } = e
&& (modifiers.command || is_function_key(key) || ui_shortcut(key, modifiers).is_some())
&& is_global_shortcut(key, modifiers)
{
dispatch(app, key, modifiers);
}
@ -438,4 +444,15 @@ mod tests {
assert!(!is_context_menu_key(Key::F10, m(true, true, false)), "Ctrl+Shift+F10");
assert!(!is_context_menu_key(Key::F9, m(true, false, false)), "Shift+F9 is a field key");
}
#[test]
fn alt_shortcuts_work_without_the_canvas_focused() {
let m = |command, alt, shift| egui::Modifiers { command, alt, shift, ..Default::default() };
assert!(is_global_shortcut(Key::Q, m(false, true, false)), "Alt+Q (#819)");
assert!(is_global_shortcut(Key::D, m(false, true, true)), "Alt+Shift+D");
assert!(is_global_shortcut(Key::B, m(true, false, false)), "Mod+B");
assert!(is_global_shortcut(Key::F7, m(false, false, false)), "F7");
assert!(!is_global_shortcut(Key::Q, m(false, false, false)), "a plain letter is typing");
assert!(!is_global_shortcut(Key::Q, m(false, false, true)), "Shift+letter is typing");
}
}

View File

@ -81,6 +81,9 @@ const TAB_LETTERS: [(&str, &str); 11] = [
("Help", "Y"),
];
/// 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("")
@ -543,6 +546,11 @@ pub fn handle_key(app: &mut WordApp, key: egui::Key) -> bool {
// File opens the Backstage: no ribbon commands to badge.
app.ui.keytips = if tab == "File" { Phase::Off } else { Phase::Commands };
}
// Q is the title bar's Search box, as in Word (#819).
None if name == SEARCH_LETTER => {
app.ui.keytips = Phase::Off;
let _ = app.run("tools.search", serde_json::json!({}));
}
None => app.ui.keytips = Phase::Off,
}
}
@ -1118,4 +1126,17 @@ mod tests {
b.apply_prefs(serde_json::from_str("{}").unwrap());
assert!(b.ui.keytips_enabled, "older preferences retain enabled KeyTips");
}
/// Alt, then Q opens Search like Alt+Q (#819); no tab owns Q.
#[test]
fn alt_then_q_opens_search() {
let mut a = app();
a.ui.backstage = false;
a.dialog = None;
assert!(TAB_LETTERS.iter().all(|(_, l)| *l != SEARCH_LETTER));
activate(&mut a);
assert!(handle_key(&mut a, egui::Key::Q));
assert_eq!(a.ui.keytips, Phase::Off);
assert!(matches!(a.dialog, Some(crate::dialogs::Dialog::Commands { .. })), "{:?}", a.dialog.as_ref().map(|d| d.name()));
}
}

View File

@ -3117,10 +3117,11 @@ mod tests {
assert_eq!(h.state().session.doc.plain_text(wordcraft_doc::StoryRef::Body), "Kilns");
}
/// #819: Alt+Q moves to "Tell me" command search, as in Word for Windows, without typing a q.
/// #819: Alt+Q opens Search with its box focused, as in Word: the Q isn't typed into the
/// document, and what is typed next goes into the search box.
#[cfg(not(target_os = "macos"))]
#[test]
fn alt_q_opens_command_search_from_the_page() {
fn alt_q_opens_search_and_typing_goes_there() {
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|ui, app: &mut WordApp| {
let ctx = ui.ctx().clone();
@ -3132,13 +3133,64 @@ mod tests {
for _ in 0..6 {
h.step();
}
let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };
h.event(egui::Event::Text("Kilns".into()));
h.step();
h.event(egui::Event::Key { key: egui::Key::Q, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::ALT });
// Some platforms send the key's text with an Alt shortcut; it isn't typed.
h.event(key(egui::Key::Q, egui::Modifiers::ALT));
h.event(egui::Event::Text("q".into()));
h.step();
assert!(matches!(h.state().dialog, Some(dialogs::Dialog::Commands { .. })), "command search is open");
let query = |h: &egui_kittest::Harness<WordApp>| match &h.state().dialog {
Some(dialogs::Dialog::Commands { query, .. }) => Some(query.clone()),
_ => None,
};
assert_eq!(query(&h).as_deref(), Some(""), "Search is open and empty");
h.step();
{
use egui_kittest::kittest::Queryable;
assert!(h.query_by_label_contains("Word Count").is_some(), "an empty box suggests commands, as Word's does");
}
h.event(egui::Event::Text("bold".into()));
h.step();
assert_eq!(query(&h).as_deref(), Some("bold"), "typing goes into the search box");
assert_eq!(h.state().session.doc.plain_text(wordcraft_doc::StoryRef::Body), "Kilns");
h.event(key(egui::Key::Escape, egui::Modifiers::NONE));
h.step();
h.step();
assert!(h.state().dialog.is_none(), "Escape closes Search");
// Words that name no command: Enter finds them in the document, in the Navigation pane.
h.event(key(egui::Key::Q, egui::Modifiers::ALT));
h.step();
h.step();
// Typing sends each key's press before its text.
h.event(key(egui::Key::K, egui::Modifiers::SHIFT));
h.event(egui::Event::Text("Kilns".into()));
h.step();
h.event(key(egui::Key::Enter, egui::Modifiers::NONE));
h.step();
h.step();
assert!(h.state().dialog.is_none(), "Enter ran the search");
assert!(h.state().session.view.nav_pane, "the Navigation pane shows the results");
assert_eq!(h.state().session.find.results.len(), 1);
}
/// #819: `tools.search` with a query lists matching commands for scripts and agents; every
/// word has to match, so "insert table" finds the command that inserts a table.
#[test]
fn search_command_matches_every_word() {
let mut a = app();
let r = a.run("tools.search", json!({"query": "insert table"})).unwrap();
let ids: Vec<&str> = r["matches"].as_array().unwrap().iter().filter_map(|m| m["id"].as_str()).collect();
assert!(ids.contains(&"insert.table"), "{ids:?}");
assert!(r["matches"].as_array().unwrap().iter().all(|m| {
let hay = format!("{} {} {}", m["label"], m["location"], m["id"]).to_lowercase();
hay.contains("insert") && hay.contains("table")
}));
assert_eq!(a.run("tools.search", json!({"query": " "})).unwrap()["matches"], json!([]));
assert_eq!(a.run("tools.search", json!({"query": "zzqqxx"})).unwrap()["matches"], json!([]));
assert!(a.dialog.is_none(), "a query never opens the box");
a.run("tools.search", json!({})).unwrap();
assert!(matches!(a.dialog, Some(dialogs::Dialog::Commands { .. })), "no query opens Search");
}
/// Save writes the document first, then carries on with what the user asked for.

View File

@ -8,6 +8,13 @@ Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-
|---|---|---|
| File | 22 | 22 |
| Quick Access | 5 | 5 |
**484 of 499 catalog features have commands (97.0%).**
| Tab | Live | Total |
|---|---|---|
| File | 21 | 21 |
| Quick Access | 4 | 4 |
| Title Bar | 1 | 1 |
| Home | 102 | 103 |
| Insert | 47 | 53 |
| Draw | 10 | 11 |

View File

@ -60,6 +60,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| 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-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-11 | trivial | Tables on the page: column and row borders and the resize handle drag, with a dashed guide and one undo step per drag (#217) |
| 2026-10-11 | trivial | Animation effects › Use system setting also reads macOS (Reduce motion) and the Linux/BSD desktops (settings portal: the cross-desktop reduced-motion preference, GNOME's switch, KDE's animation speed; the GTK and KDE settings files where no portal answers), and follows a change without a restart |
| 2026-10-11 | trivial | Animation effects setting in File › Options › Accessibility (Use system setting, On, Off), kept between runs: the app eased wheel scrolling, zooming and its fades whatever the system's setting was; off, they land at once. Follows the system's setting on Windows and in browsers; `ui.animations` for agents |