mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-13 01:31:18 +00:00
#3518: Review › Language can set the whole document's proofing language
review.language takes scope "selection" (default, unchanged) or "document": every run and paragraph mark of every story (body, headers, footers, notes, comments, text boxes, table cells, glossary) takes lang/noProof in one undo step, selection unchanged, pending typing format follows. The Language dialog gets an "Apply to the whole document" check box. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FypgCVCCEo7u5wetHaJRU4
This commit is contained in:
1 parent
729cd0a5fc
commit
c09da43475
7 files changed
+218
-10
No files matched your search
+2
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** trivial (Protected typing boundary lookup counts preceding objects once and indexes editable permission IDs (#3614); caret movement and enforcement policy are unchanged; previously trivial: progress log: Custom document properties dialog, #2982; previously trivial: progress log: View › New Window; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial (progress log: a shape dropped just after a page break inside a paragraph stays on the new page, #4262); previously trivial (progress log: a float anchored on a later page of a long paragraph is drawn there, #4250); previously minor (progress log: `file.closeDocument` over the control channel and MCP bridge is refused while the document it names has unsaved changes; previously trivial (progress log: ODT element audit, ODT checked against 220 files from other applications, ODT display styles); previously trivial (progress log: Shape Format Line truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** trivial (Language dialog can set the whole document's proofing language, #3518; previously trivial: Protected typing boundary lookup counts preceding objects once and indexes editable permission IDs (#3614); caret movement and enforcement policy are unchanged; previously trivial: progress log: Custom document properties dialog, #2982; previously trivial: progress log: View › New Window; previously trivial: progress log: interface translations come from storytold/craft-i18n through the `craft-i18n` crate; previously trivial: progress log: disabled-control reasons translated in every interface language, #2678; previously trivial: progress log: mail merge record navigation and record ranges for printing, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: group connector rerouting preserves rotated and flipped free or unresolved endpoints under nonuniform group scaling; readiness estimates unchanged; previously trivial: progress log: a word with no break opportunity in it no longer makes the layout quadratic, #3412; previously trivial: progress log: a relationship the markup names without the prefix is read as it wrote, and an ordinary `id` names no part; previously trivial: progress log: a document with many relationships opens in the time its part takes, and an id the text only mentions is not a reference, #3411; previously trivial: progress log: read only and comments-only protection grey out edits; previously trivial: progress log: an empty paragraph with a hidden mark shows nothing, not even its list number, #3982; previously trivial: progress log: "NaN" in the Font dialog's Size box no longer closes the app, #3663; previously trivial: progress log: a table cell span from a `.json` file no longer overflows, #3637; previously trivial: progress log: a click in the footer while editing the header moves there; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Slovenian interface catalog, 2,614 entries; previously trivial: Insert Table of Figures dialog for any caption label, #3013; previously trivial: progress log: Home › Underline drop-down, #2738; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: progress log: the scrollbar page tip stays on one line, #3341; previously trivial: progress log: forms protection greys out the control buttons; previously trivial: progress log: a document with many content controls saves in the time the controls take, #13; previously trivial: progress log: Word signature lines kept on save; previously trivial: a Thai run with Latin finds one installed font covering it all on Linux too, #1549; previously trivial: progress log: closing the header returns to the body selection, #3331; previously trivial: progress log: saving to a NAS share mounted by GVFS, #3003; previously trivial: progress log: a picture dragged near the bottom of a page lands there, #3169; previously trivial: centred list labels after leading page breaks, #2964; previously trivial: progress log: inline pictures drag to an insertion cursor and stay inline, #2233, #2234; previously trivial: progress log: a text file saved over itself closes without asking, #2376; previously trivial: progress log: a text frame in the paragraph defaults frames nothing, #2360; previously trivial: fixed font inputs in four layout controls; previously trivial: typing no longer starts render threads on every repaint, #3027; previously trivial: a right, centre or decimal tab whose text doesn't fit, or a left tab past the right indent, goes to the next line with its text, #1840; previously trivial: progress log: banded rows shade as in Word, #3415; previously trivial: progress log: the table handles show while the caret is in the table, #2428; previously trivial: progress log: a click below a floating table reaches the text after it, #2038; previously trivial: progress log: Delete on selected table cells leaves one empty paragraph in each, #2802; previously trivial: progress log: lam-alef in exported PDFs extracts whole with fonts that draw it as two glyphs, #2310; previously trivial: Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459); previously trivial (progress log: a shape dropped just after a page break inside a paragraph stays on the new page, #4262); previously trivial (progress log: a float anchored on a later page of a long paragraph is drawn there, #4250); previously minor (progress log: `file.closeDocument` over the control channel and MCP bridge is refused while the document it names has unsaved changes; previously trivial (progress log: ODT element audit, ODT checked against 220 files fromLine truncated
|
||||
>
|
||||
> **Recipient CSV update:** 2026-10-27 · Complete quoted header records choose the delimiter (#4249); detection and regressions follow the current mailings module split. Recipient limits and readiness estimates are unchanged.
|
||||
>
|
||||
@@ -144,6 +144,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-11-06: Review › Language: "Apply to the whole document" sets the proofing language or Do not check spelling for every story (body, headers, footers, notes, comments, text boxes, table cells, glossary) in one undo step, selection unchanged; `review.language` takes `scope: "document"`, default `selection` (#3518). Readiness estimates unchanged.
|
||||
- 2026-11-06: Windows print submissions run in separate helper processes (#5710). A helper acknowledges cancellation before it stops, so a driver stalled in a cancelled job cannot hold another job’s provider lock. A private temporary PDF and bounded settings messages cross the process boundary; normal shutdown reaps helpers and removes their inputs. The helper erases its plaintext inputs before driver work; a hard app crash during startup can still leave private temporary files. Jobs already sending still complete. Readiness estimates unchanged.
|
||||
- 2026-11-06: File › Print: before sending starts, Cancel printing stops a waiting job, frees the Print page and keeps the settings; a successfully cancelled job is never sent. Cancellation remains available after the last document closes. Quitting cancels waiting jobs and joins jobs already sending (#5710). On Windows, winprint repeats the DEVMODE conversion after sending starts. A stall during that repeat prevents cancellation and can keep shutdown waiting, even before a native job exists. Readiness estimates unchanged.
|
||||
- 2026-11-06: File › Print on Windows no longer waits on printer drivers to list printers (#5710). The list used to wait until each printer's driver said whether it prints on both sides, and a networked printer asleep in power save could keep it empty with Refresh unavailable; now discovery asks no driver (the Windows spooler still supplies the list), and printing still checks the job's options with the driver. The reporter's sleeping printer has not been retested yet. Readiness estimates unchanged.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{Arc, CmdError, CmdResult, Document, Pos, Selection, Session, StoryRef, Value, delete_selection, json, p, sel_result};
|
||||
use wordcraft_doc::CharProps;
|
||||
|
||||
/// Options › Language › Set as default: only the saved default proofing language (what future
|
||||
/// blank documents are written in), as Word's Set as Preferred. A setting, like `file.options`: the
|
||||
@@ -14,8 +15,15 @@ pub(super) fn preferred_language(s: &mut Session, v: &Value) -> CmdResult {
|
||||
/// Set Proofing Language: the selection's language and "do not check spelling or grammar",
|
||||
/// or (`default`) the document's default and the saved language for future blank documents;
|
||||
/// `detect` is the per-user detection preference.
|
||||
/// Nothing at all sets US English, as before.
|
||||
/// Nothing at all sets US English, as before. `scope: "document"` sets it for every story.
|
||||
pub(super) fn language(s: &mut Session, v: &Value) -> CmdResult {
|
||||
// Checked first, so a bad scope changes nothing (not even the detect preference).
|
||||
let whole_document = match v.get("scope") {
|
||||
None | Some(Value::Null) => false,
|
||||
Some(Value::String(t)) if t == "selection" => false,
|
||||
Some(Value::String(t)) if t == "document" => true,
|
||||
Some(other) => return Err(CmdError::Params(format!("`scope` must be \"selection\" or \"document\", not {other}"))),
|
||||
};
|
||||
let lang = match p::str(v, "lang") {
|
||||
Some(t) => Some(
|
||||
wordcraft_proof::lang::normalize_tag(t)
|
||||
@@ -45,20 +53,75 @@ pub(super) fn language(s: &mut Session, v: &Value) -> CmdResult {
|
||||
if lang.is_none() && no_proof.is_none() {
|
||||
return Ok(json!({"detect": s.prefs.detect_language}));
|
||||
}
|
||||
let result = crate::cmd::format::apply(s, &|c| {
|
||||
let change = |c: &mut CharProps| {
|
||||
if let Some(l) = &lang {
|
||||
c.lang = Some(l.clone());
|
||||
}
|
||||
if let Some(np) = no_proof {
|
||||
c.no_proof = Some(np);
|
||||
}
|
||||
})?;
|
||||
};
|
||||
let result = if whole_document {
|
||||
set_document_props(s, &change)?;
|
||||
json!({"scope": "document"})
|
||||
} else {
|
||||
crate::cmd::format::apply(s, &change)?
|
||||
};
|
||||
if p::bool(v, "default") == Some(true) {
|
||||
s.prefs.default_language = lang;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Apply `f` to the character formatting of every story of the document (Language dialog, "Apply
|
||||
/// to the whole document"): text and paragraph marks of the body, headers, footers, notes,
|
||||
/// comments, text boxes (all `Document::parts`), table cells, and the glossary's building blocks.
|
||||
/// The selection is untouched; formatting waiting for typing follows.
|
||||
fn set_document_props(s: &mut Session, f: &dyn Fn(&mut CharProps)) -> Result<(), CmdError> {
|
||||
let stories: Vec<StoryRef> = std::iter::once(StoryRef::Body).chain(s.doc.parts.keys().map(|k| StoryRef::Part(*k))).collect();
|
||||
for story in stories {
|
||||
for path in s.doc.para_paths(story) {
|
||||
let para = s.doc.para_mut(story, &path)?;
|
||||
let len = para.len();
|
||||
para.format(0, len, f)?;
|
||||
f(&mut para.mark);
|
||||
para.touch();
|
||||
}
|
||||
}
|
||||
for block in &mut s.doc.glossary {
|
||||
format_blocks(&mut block.blocks, f, 0);
|
||||
}
|
||||
if let Some(pend) = s.pending.as_mut() {
|
||||
f(pend);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// [`set_document_props`] for a block list outside any story (depth-limited like `para_paths`).
|
||||
fn format_blocks(blocks: &mut wordcraft_doc::Blocks, f: &dyn Fn(&mut CharProps), depth: usize) {
|
||||
if depth > 32 {
|
||||
return;
|
||||
}
|
||||
for blk in blocks.iter_mut() {
|
||||
match Arc::make_mut(blk) {
|
||||
wordcraft_doc::Block::Para(p) => {
|
||||
let len = p.len();
|
||||
// A range inside the paragraph can't fail; the mark is formatted regardless.
|
||||
let _ = p.format(0, len, f);
|
||||
f(&mut p.mark);
|
||||
p.touch();
|
||||
}
|
||||
wordcraft_doc::Block::Table(t) => {
|
||||
for row in &mut t.rows {
|
||||
for cell in &mut row.cells {
|
||||
format_blocks(&mut cell.blocks, f, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn macros(s: &mut Session, v: &Value) -> CmdResult {
|
||||
if let Some(d) = v.get("define") {
|
||||
let name = d.get("name").and_then(Value::as_str).unwrap_or("Macro").to_string();
|
||||
|
||||
@@ -340,7 +340,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
})
|
||||
.params(r#"{"format": "decimal|upperRoman|lowerLetter…", "text"?: "%1.", "start"?: n}"#),
|
||||
CommandSpec::new("review.language", "Language", "Review › Language", language).params(
|
||||
r#"{"lang"?: "en-US|en-GB|pl-PL|fr-FR|…" (BCP 47; default en-US), "noProof"?: bool, "detect"?: bool (detect language automatically, a preference), "default"?: bool (also the document's default and the saved language for future blank documents)}"#,
|
||||
r#"{"lang"?: "en-US|en-GB|pl-PL|fr-FR|…" (BCP 47; default en-US), "noProof"?: bool, "detect"?: bool (detect language automatically, a preference), "default"?: bool (also the document's default and the saved language for future blank documents), "scope"?: "selection" (default) | "document" (every run and paragraph mark of every story: body, headers, footers, notes, comments, text boxes, tables, glossary; one undo step, selection unchanged)}"#,
|
||||
),
|
||||
CommandSpec::new("review.language.preferred", "Set as Default Language", "File › Options › Language", preferred_language)
|
||||
.params(r#"{"lang": "en-US|en-GB|pl-PL|nl-NL|…" (BCP 47)} — only the saved default proofing language, what future blank documents are written in, as Options › Language › Set as default does; the open document, the selection and the undo history don't change. Returns {"preferred": tag}."#)
|
||||
|
||||
@@ -290,3 +290,104 @@ fn inspect_removals_work_when_the_document_does_not_track_changes() {
|
||||
assert!(!text.contains("Hidden"), "the hidden text is still there: {text}");
|
||||
assert!(text.contains("Visible text."), "{text}");
|
||||
}
|
||||
|
||||
/// #3518: every run and paragraph mark of a story, as (story, path, lang, noProof) rows.
|
||||
fn proofing_rows(s: &Session) -> Vec<(StoryRef, Option<String>, Option<bool>)> {
|
||||
let stories = std::iter::once(StoryRef::Body).chain(s.doc.parts.keys().map(|k| StoryRef::Part(*k)));
|
||||
let mut out = Vec::new();
|
||||
for st in stories {
|
||||
for path in s.doc.para_paths(st) {
|
||||
let Some(p) = s.doc.para(st, &path) else { continue };
|
||||
out.push((st, p.mark.lang.clone(), p.mark.no_proof));
|
||||
out.extend(p.runs.iter().map(|r| (st, r.props.lang.clone(), r.props.no_proof)));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A document with its text in German in the body, a table cell, a header, a footnote, a comment
|
||||
/// and a text box, as a Word DOCX keeps a `w:lang` on each run, plus a glossary entry.
|
||||
fn multi_story_doc() -> Session {
|
||||
use wordcraft_doc::{Block, BuildingBlock, CharProps, Paragraph, PartKind, Table};
|
||||
let de = || CharProps { lang: Some("de-DE".into()), ..Default::default() };
|
||||
let blk = |t: &str| Arc::new(Block::Para(Paragraph::with_text(t, de())));
|
||||
let mut d = Document::from_text("Guten Tag");
|
||||
d.body = vec![blk("Guten Tag")];
|
||||
let mut t = Table::new(1, 2, 200.0);
|
||||
if let Some(c) = t.rows.first_mut().and_then(|r| r.cells.first_mut()) {
|
||||
c.blocks = vec![blk("Zelle")];
|
||||
}
|
||||
d.body.push(Arc::new(Block::Table(t)));
|
||||
d.body.push(blk("Ende"));
|
||||
for kind in [PartKind::Header, PartKind::Footnote, PartKind::Comment, PartKind::TextBox] {
|
||||
d.add_part(kind, vec![blk("Teil")]);
|
||||
}
|
||||
d.glossary.push(BuildingBlock { name: "g".into(), blocks: vec![blk("Baustein")], ..Default::default() });
|
||||
Session::new(d)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_scope_document_sets_every_story() {
|
||||
let mut s = multi_story_doc();
|
||||
s.run("select.text", &json!({"text": "Guten"})).unwrap();
|
||||
let (sel, before) = (s.sel.clone(), s.doc.clone());
|
||||
let rows = proofing_rows(&s);
|
||||
assert!(rows.len() >= 10 && rows.iter().any(|r| r.1.as_deref() == Some("de-DE")));
|
||||
s.run("review.language", &json!({"lang": "fr-FR", "scope": "document"})).unwrap();
|
||||
// Runs that had their own language, the paragraph marks and the empty cell: all French.
|
||||
let rows = proofing_rows(&s);
|
||||
assert!(rows.iter().all(|r| r.1.as_deref() == Some("fr-FR")), "{rows:?}");
|
||||
for kind_story in s.doc.parts.keys() {
|
||||
assert!(rows.iter().any(|r| r.0 == StoryRef::Part(*kind_story)));
|
||||
}
|
||||
assert!(s.doc.plain_text(StoryRef::Body).contains("Zelle"));
|
||||
let g = &s.doc.glossary[0].blocks;
|
||||
assert_eq!(g[0].as_para().map(|p| p.runs[0].props.lang.clone()), Some(Some("fr-FR".into())));
|
||||
assert_eq!(g[0].as_para().map(|p| p.mark.lang.clone()), Some(Some("fr-FR".into())));
|
||||
// The selection stays; the document is modified.
|
||||
assert_eq!(s.sel, sel);
|
||||
assert!(s.doc != before);
|
||||
// One undo restores everything.
|
||||
s.run("edit.undo", &json!({})).unwrap();
|
||||
assert!(s.doc == before);
|
||||
assert_eq!(s.sel, sel);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_scope_document_no_proof_and_pending() {
|
||||
let mut s = multi_story_doc();
|
||||
s.run("review.language", &json!({"noProof": true, "scope": "document"})).unwrap();
|
||||
assert!(proofing_rows(&s).iter().all(|r| r.2 == Some(true)));
|
||||
// The language itself wasn't asked for and stays.
|
||||
assert!(proofing_rows(&s).iter().any(|r| r.1.as_deref() == Some("de-DE")));
|
||||
// Formatting waiting for typing follows.
|
||||
s.run("format.bold", &json!({})).unwrap();
|
||||
s.run("review.language", &json!({"lang": "pl-PL", "scope": "document"})).unwrap();
|
||||
assert_eq!(s.pending.as_ref().and_then(|p| p.lang.as_deref()), Some("pl-PL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_default_scope_is_selection_only() {
|
||||
let mut s = multi_story_doc();
|
||||
s.run("select.text", &json!({"text": "Guten"})).unwrap();
|
||||
s.run("review.language", &json!({"lang": "fr-FR"})).unwrap();
|
||||
let rows = proofing_rows(&s);
|
||||
assert!(rows.iter().any(|r| r.1.as_deref() == Some("fr-FR")));
|
||||
assert!(rows.iter().filter(|r| r.1.as_deref() == Some("fr-FR")).count() < rows.len() / 2);
|
||||
let mut s = multi_story_doc();
|
||||
s.run("select.text", &json!({"text": "Guten"})).unwrap();
|
||||
s.run("review.language", &json!({"lang": "fr-FR", "scope": "selection"})).unwrap();
|
||||
assert_eq!(proofing_rows(&s), rows);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn language_bad_scope_is_a_params_error() {
|
||||
let mut s = multi_story_doc();
|
||||
let before = s.doc.clone();
|
||||
for bad in [json!("everything"), json!(1), json!(true), json!(["document"])] {
|
||||
let e = s.run("review.language", &json!({"lang": "fr-FR", "detect": false, "scope": bad})).unwrap_err();
|
||||
assert!(matches!(e, CmdError::Params(_)), "{e:?}");
|
||||
}
|
||||
assert!(s.doc == before);
|
||||
assert!(s.prefs.detect_language);
|
||||
}
|
||||
@@ -208,7 +208,12 @@ fn language_dialog_default_survives_a_restart() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let mut a = app();
|
||||
a.ui.language = "en".into();
|
||||
a.dialog = Some(Dialog::Layout(Box::new(LayoutDialog::Language(LanguageForm { lang: "fr-FR".into(), no_proof: false, detect: false }))));
|
||||
a.dialog = Some(Dialog::Layout(Box::new(LayoutDialog::Language(LanguageForm {
|
||||
lang: "fr-FR".into(),
|
||||
no_proof: false,
|
||||
detect: false,
|
||||
whole_document: false,
|
||||
}))));
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
@@ -232,6 +237,36 @@ fn language_dialog_default_survives_a_restart() {
|
||||
assert_eq!(LanguageForm::read(&b).lang, "fr-FR");
|
||||
}
|
||||
|
||||
/// #3518: ticking "Apply to the whole document" makes OK send `scope: "document"`; unticked sends none.
|
||||
#[test]
|
||||
fn language_dialog_whole_document_checkbox_sends_scope() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let f = LanguageForm { lang: "fr-FR".into(), no_proof: false, detect: false, whole_document: false };
|
||||
assert!(f.params().get("scope").is_none());
|
||||
assert_eq!(LanguageForm { whole_document: true, ..f.clone() }.params()["scope"], "document");
|
||||
let mut a = app();
|
||||
a.ui.language = "en".into();
|
||||
let para = wordcraft_doc::Paragraph::with_text("Header", Default::default());
|
||||
let header = a.session.doc.add_part(wordcraft_doc::PartKind::Header, vec![Arc::new(wordcraft_doc::Block::Para(para))]);
|
||||
a.dialog = Some(Dialog::Layout(Box::new(LayoutDialog::Language(f))));
|
||||
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state(
|
||||
|ui, app: &mut WordApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
},
|
||||
a,
|
||||
);
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Apply to the whole document").click();
|
||||
h.run_steps(3);
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(3);
|
||||
// The header, which the caret isn't in, took the language.
|
||||
let doc = &h.state().session.doc;
|
||||
let hp = doc.para(wordcraft_doc::StoryRef::Part(header), &wordcraft_doc::Path::top(0)).unwrap();
|
||||
assert_eq!(hp.runs.first().and_then(|r| r.props.lang.as_deref()), Some("fr-FR"));
|
||||
}
|
||||
|
||||
/// #407: Envelopes starts from the selected address; Add to Document puts an envelope
|
||||
/// section before the letter without asking to save (nothing is replaced).
|
||||
#[test]
|
||||
|
||||
@@ -320,6 +320,8 @@ pub struct LanguageForm {
|
||||
pub lang: String,
|
||||
pub no_proof: bool,
|
||||
pub detect: bool,
|
||||
/// "Apply to the whole document": OK sends `scope: "document"` (every story), not the selection.
|
||||
pub whole_document: bool,
|
||||
}
|
||||
|
||||
impl LanguageForm {
|
||||
@@ -328,12 +330,16 @@ impl LanguageForm {
|
||||
let style = s.doc.para_at(&s.sel.focus).and_then(|p| p.props.style.as_deref());
|
||||
let here = s.doc.styles.resolve_char(style, &s.typing_props());
|
||||
let lang = here.lang.unwrap_or_else(|| "en-US".into());
|
||||
LanguageForm { lang, no_proof: here.no_proof, detect: s.prefs.detect_language }
|
||||
LanguageForm { lang, no_proof: here.no_proof, detect: s.prefs.detect_language, whole_document: false }
|
||||
}
|
||||
|
||||
/// `review.language` parameters for OK.
|
||||
pub fn params(&self) -> Value {
|
||||
json!({"lang": self.lang, "noProof": self.no_proof, "detect": self.detect})
|
||||
let mut v = json!({"lang": self.lang, "noProof": self.no_proof, "detect": self.detect});
|
||||
if let (true, Some(m)) = (self.whole_document, v.as_object_mut()) {
|
||||
m.insert("scope".into(), json!("document"));
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,6 +366,7 @@ pub(super) fn language_ui(app: &mut WordApp, ui: &mut Ui, f: &mut LanguageForm)
|
||||
ui.add_space(4.0);
|
||||
ui.checkbox(&mut f.no_proof, tl!("Do not check spelling or grammar"));
|
||||
ui.checkbox(&mut f.detect, tl!("Detect language automatically"));
|
||||
ui.checkbox(&mut f.whole_document, tl!("Apply to the whole document"));
|
||||
ui.add_space(4.0);
|
||||
let set_default = ui.button(tl!("Set As Default")).clicked();
|
||||
if set_default && run(app, "review.language", json!({"lang": f.lang, "default": true, "detect": f.detect})) {
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-04 · **Change:** trivial (Protected typing boundary lookup counts preceding objects once and indexes editable permission IDs (#3614); caret movement and enforcement policy are unchanged; previously trivial: Custom Document Properties dialog from File › Info › Properties › Advanced Properties…, #2982; previously trivial: View › Window › New Window opens another window on the document, each with its own caret and view; previously trivial: mail merge: First Record, Last Record and a record number box in Preview Results; Finish & Merge asks which records to merge or print, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: read only and comments-only protection grey out edits; previously trivial: the Font dialog previews "NaN" or "inf" in the Size box at 12 pt, #3663; previously trivial: File › Options disables the User name field while Restrict Editing is active; previously trivial: while editing a header or footer, a click in the other one edits it there without scrolling; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Insert Table of Figures dialog: any caption label, #3013; previously trivial: Home › Underline drop-down shows its lines, #2738; previously trivial: the text context menu includes the formatting mini toolbar, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: the scrollbar page tip stays on one line as the page number grows, #3341; previously trivial: File › Home and New: templates and recent documents are buttons named for screen readers, #3176; previously trivial: forms protection greys out the Developer tab's control buttons and Properties; previously trivial: AltGr doesn't open KeyTips, #2388; previously trivial: closing the header or footer puts the caret back where it was in the body, #3331; previously trivial: Format Page Numbers dialog: number format and Continue from previous section / Start at, #1113; previously trivial: inline pictures drag to a dashed insertion cursor and stay inline, #2233, #2234; previously trivial: Save Changes prompt: a text file saved over itself closes, #2376; previously trivial: the equation being edited is framed as in Word: an outline as tall as its line, a grip, an options strip that opens its menu, and its current argument shaded; previously trivial: the table handles show while the caret is in the table, #2428; previously trivial: legacy menu keys, #1697; previously trivial: Shape Fill › Gradient and the Format Shape pane, #1500; previously trivial: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: the text pointer over the page is drawn black or white on Windows, #1746; previously trivial: a click in the header/footer band from the body no longer scrolls to the body text, #2103; previously trivial: File › Recover follows the app theme, #3243; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: dragging the vertical scrollbar names the page at the top of the window, #2551; previously trivial: Add Font imports every selected font file, #1789); previously trivial (dragging an In Line with Text object shows a bold insertion cursor, as in Word, instead of a faint dashed one, #4195); previously trivial (the shapes, pictures and text boxes inside a Drawing Canvas are picked, moved and resized on their own, #2637); previously trivial (Shape Format › Size shows the interface unit and takes any typed unit, #4196); previously trivial (over a turned picture, shape or text box the resize arrows turn with it, #3932); previously trivial (the status bar's count is words by default and can be characters with or without spaces, chosen from its right-click menu or File › Options › Display and kept between runs, #2797); previously trivial (Linux display server setting in File › Options › Advanced, #2184); previously trivial (dialogs are modal except Find and Replace, Symbol, Mark Index Entry, Cross-reference and Insert Merge Field, and a press beside one shakes it, #3859); previously trivial (showing the Format Shape pane leaves a gradient angle above 359.9° as it is, #3955); previously trivial (Return on File › Print prints, as Word's default Print button does); previously trivial (N-up and booklet printing, #641; readiness unchanged); previously trivial (missing fonts flagged in the font box and status bar, read from what layout drew; metric-compatible substitutes and symbol fonts not flagged); previously minor (Options › Language redesign: Your languages, Add a language…, Set as default; the Dictionaries folder moved from Proofing); previously trivial (a long comment's Line truncated
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** trivial (Language dialog: Apply to the whole document sets the proofing language or no-proof for every story in one undo step, #3518; previously trivial: Protected typing boundary lookup counts preceding objects once and indexes editable permission IDs (#3614); caret movement and enforcement policy are unchanged; previously trivial: Custom Document Properties dialog from File › Info › Properties › Advanced Properties…, #2982; previously trivial: View › Window › New Window opens another window on the document, each with its own caret and view; previously trivial: mail merge: First Record, Last Record and a record number box in Preview Results; Finish & Merge asks which records to merge or print, #3028; previously trivial: selection bar for right-to-left paragraphs and table rows; previously trivial: read only and comments-only protection grey out edits; previously trivial: the Font dialog previews "NaN" or "inf" in the Size box at 12 pt, #3663; previously trivial: File › Options disables the User name field while Restrict Editing is active; previously trivial: while editing a header or footer, a click in the other one edits it there without scrolling; previously trivial: explicit documents keep the editor when recovery discovery finishes; readable recovery controls; previously trivial: Insert Table of Figures dialog: any caption label, #3013; previously trivial: Home › Underline drop-down shows its lines, #2738; previously trivial: the text context menu includes the formatting mini toolbar, #2959; previously trivial: personal templates in File › New and Save as Word Template, #2852; previously trivial: the scrollbar page tip stays on one line as the page number grows, #3341; previously trivial: File › Home and New: templates and recent documents are buttons named for screen readers, #3176; previously trivial: forms protection greys out the Developer tab's control buttons and Properties; previously trivial: AltGr doesn't open KeyTips, #2388; previously trivial: closing the header or footer puts the caret back where it was in the body, #3331; previously trivial: Format Page Numbers dialog: number format and Continue from previous section / Start at, #1113; previously trivial: inline pictures drag to a dashed insertion cursor and stay inline, #2233, #2234; previously trivial: Save Changes prompt: a text file saved over itself closes, #2376; previously trivial: the equation being edited is framed as in Word: an outline as tall as its line, a grip, an options strip that opens its menu, and its current argument shaded; previously trivial: the table handles show while the caret is in the table, #2428; previously trivial: legacy menu keys, #1697; previously trivial: Shape Fill › Gradient and the Format Shape pane, #1500; previously trivial: Focus fills the screen, and Full Screen View shows the pages alone, #1678; previously trivial: the text pointer over the page is drawn black or white on Windows, #1746; previously trivial: a click in the header/footer band from the body no longer scrolls to the body text, #2103; previously trivial: File › Recover follows the app theme, #3243; previously trivial: long navigation headings wrap onto further lines within the pane, #3015; previously trivial: clearing a no-match font query keeps the recent heading visible, #2068; previously trivial: dragging the vertical scrollbar names the page at the top of the window, #2551; previously trivial: Add Font imports every selected font file, #1789); previously trivial (dragging an In Line with Text object shows a bold insertion cursor, as in Word, instead of a faint dashed one, #4195); previously trivial (the shapes, pictures and text boxes inside a Drawing Canvas are picked, moved and resized on their own, #2637); previously trivial (Shape Format › Size shows the interface unit and takes any typed unit, #4196); previously trivial (over a turned picture, shape or text box the resize arrows turn with it, #3932); previously trivial (the status bar's count is words by default and can be characters with or without spaces, chosen from its right-click menu or File › Options › Display and kept between runs, #2797); previously trivial (Linux display server setting in File › Options › Advanced, #2184); previously trivial (dialogs are modal except Find and Replace, Symbol, Mark Index Entry, Cross-reference and Insert Merge Field, and a press beside one shakes it, #3859); previously trivial (showing the Format Shape pane leaves a gradient angle above 359.9° as it is, #3955); previously trivial (Return on File › Print prints, as Word's default Print button does); previously trivial (N-up and booklet printing, #641; readiness unchanged); previously trivial (missing fonts flagged in the font box and status bar, read from what layout drew; metric-compatible substitutes and symbol fonts not flagged); previously minor (Options › LanguageLine truncated
|
||||
|
||||
Recovery discovery no longer interrupts an explicitly created or opened document. Older copies remain in File › Recover, whose buttons use the interface palette. See [local recovery](recovery.md).
|
||||
|
||||
@@ -72,7 +72,7 @@ Document title opens Rename Document; the account badge opens Options › Genera
|
||||
|
||||
Save Changes exposes the document-specific question as an alert-dialog name and the loss warning plus any failed-save explanation as its description (#1015). Full-app AccessKit coverage checks the focused choice's dialog ancestry, all three choices and cancellation. This is accessibility-tree evidence; native Windows screen-reader and spoken VoiceOver verification remain pending. Visual layout, wording and save policy are unchanged.
|
||||
|
||||
The Language dialog reads effective character formatting, including document defaults, paragraph styles and character styles. Accepting it unchanged keeps inherited language and no-proof choices (#2717); explicit direct formatting still takes precedence. Other proofing and Windows-specific reports remain separate.
|
||||
The Language dialog reads effective character formatting, including document defaults, paragraph styles and character styles. Accepting it unchanged keeps inherited language and no-proof choices (#2717); explicit direct formatting still takes precedence. The "Apply to the whole document" check box sends `review.language` `scope: "document"`: every run and paragraph mark in the body, headers, footers, notes, comments, text boxes, table cells and glossary takes the language and/or no-proof, as one undo step with the selection unchanged (#3518). Other proofing and Windows-specific reports remain separate.
|
||||
|
||||
## Find and Replace: regular expressions (#4177)
|
||||
|
||||
@@ -101,6 +101,7 @@ Right-click and Shift+F10 keep the compact suggestions menu. The pane is shared
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-06 | trivial | Language dialog: Apply to the whole document sets the proofing language or Do not check for every story (body, headers, footers, notes, comments, text boxes, tables, glossary) in one undo step; `review.language` takes `scope: "document"` (#3518) |
|
||||
| 2026-10-17 | trivial | Drawing Canvas members are selected with a click and dragged, resized, turned, nudged, formatted, deleted and copied on their own inside the canvas; a click in a text box member's text puts the caret there (#2637) |
|
||||
| 2026-11-05 | trivial | On-canvas objects: dragging an In Line with Text picture, shape or text box shows a bold, solid insertion cursor, twice the caret's width, as in Word; the dashed one was hard to see (#4195) |
|
||||
| 2026-10-28 | trivial | Protected typing boundary lookup counts preceding objects once and indexes editable permission IDs (#3614); caret movement and enforcement policy are unchanged. |
|
||||
|
||||
Reference in new issue
Block a user