* 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:
parent
8c8d5611a4
commit
03274b3b4f
|
|
@ -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) |
|
||||
|---|---|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue