From 2d3997d2e3438b41789575c2e132dd280bf747e9 Mon Sep 17 00:00:00 2001 From: Katacc Date: Wed, 21 Oct 2026 16:03:52 +0300 Subject: [PATCH] Review: proofing language for the whole presentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review › Language gets "Apply to: Selected text / Whole presentation". The new format.languageAll command sets lang/noProof on every run in the deck (slides, groups, table cells, notes, masters, layouts) and on the master and default text styles, so new text inherits it. It needs no selection and is one undo step; format.language is unchanged. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 + crates/engine/src/cmd/format.rs | 66 +++++++++++++++++++++++++++++++-- crates/engine/src/tests.rs | 38 +++++++++++++++++++ crates/ui-egui/src/dialogs.rs | 18 ++++++++- crates/ui-egui/src/lib.rs | 35 +++++++++++++++++ docs/gaps.md | 5 ++- docs/localization-parity.md | 5 ++- 7 files changed, 160 insertions(+), 9 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 73b62ee6..aeb57551 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,6 +6,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (fill and outline edits reach group members, #1079; stage and scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-20 · **Change:** trivial (empty chart number-format sections hide values; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-18 · **Change:** minor (Symbol-font characters (U+F0xx) remap to the installed Symbol face's real Unicode on macOS, so they draw from Symbol instead of a look-alike; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-21 · **Change:** minor (proofing language for the whole presentation; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere @@ -147,6 +148,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-22 | Shape Fill and Shape Outline format the members of a selected group, including nested groups, in one undo step. Fill transparency retains each member's effective colour (#1079) | | 2026-10-21 | 3-D contours, extrusion depths and bevels keep their size in points inside a group whose child coordinates scale its shapes: a 1 pt contour on a small extruded cube in a legacy group drew hundreds of points wide, a band across the slide | | 2026-10-21 | Groups nest at most 32 deep (`arrange.group` refuses more), as deep as PPTX import reads and slides draw: a deck grouped 39 or more levels deep saved without complaint and then could not be opened again | +| 2026-10-21 | Review › Language has *Whole presentation*: one proofing language for every text box, table cell, note, master and layout, and the default for new text (`format.languageAll`, one undo step); the per-selection language still works | | 2026-10-20 | Chart number formats with explicit empty positive, negative or zero sections hide the corresponding labels, including all values with three semicolons; a blank whole format still uses General. Synthetic formatter regressions | | 2026-10-22 | Chart bars and their legend keys draw the series' own pattern fill (`a:pattFill`, now read and written) and draw nothing for `a:noFill`, with the series outline where it has one, as PowerPoint for Mac does (synthetic probe); they took the palette's colour before. Lines, areas and markers use a pattern's foreground colour | | 2026-10-21 | Reuse Slides: pick which slides of the other presentation to insert from a list (`slide.reuseList`, #1103) | diff --git a/crates/engine/src/cmd/format.rs b/crates/engine/src/cmd/format.rs index a3b2c40a..dac13b18 100644 --- a/crates/engine/src/cmd/format.rs +++ b/crates/engine/src/cmd/format.rs @@ -6,6 +6,7 @@ use deckcraft_model::resolve; use deckcraft_model::text::{Align, Anchor, AutoFit, Bullet, Caps, ParaProps, RunProps, Spacing, Strike, TabStop, TextDir}; use deckcraft_model::{ColorRef, ColorTransform, Effects, Fill, Line, SchemeSlot, Shape, ShapeKind}; use serde_json::{Value, json}; +use std::sync::Arc; use super::text::{format_body, format_para, format_run}; use super::*; @@ -60,6 +61,15 @@ pub fn specs() -> Vec { has_text_or_shapes, language ), + cmd!( + "format.languageAll", + "Language for All Text", + ["Review", "Language"], + None, + "{lang?, noProof?: bool} (proofing language of every text in the presentation: slides, tables, notes, masters and layouts, and the default for new text; one undo step)", + has_doc, + language_all + ), cmd!("format.highlight", "Text Highlight Color", ["Home", "Font"], None, "{color? (absent = none)}", has_text_or_shapes, highlight), cmd!( "format.case", @@ -398,16 +408,17 @@ fn text_transparency(s: &mut Session, p: &Value) -> Result { r.fill = Some(f); }) } -fn language(s: &mut Session, p: &Value) -> Result { +/// The `lang` and `noProof` params of the language commands, checked: a run-props setter. +fn language_edit(id: &str, p: &Value) -> Result { let lang = str_param(p, "lang").map(str::to_string); let no_proof = bool_param(p, "noProof"); if lang.is_none() && no_proof.is_none() { - return Err(bad("format.language", "give `lang` or `noProof`")); + return Err(bad(id, "give `lang` or `noProof`")); } if lang.as_deref().is_some_and(|l| l.is_empty() || l.len() > 35 || !l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')) { - return Err(bad("format.language", "`lang` is a language tag such as en-US")); + return Err(bad(id, "`lang` is a language tag such as en-US")); } - format_run(s, &move |r| { + Ok(move |r: &mut RunProps| { if let Some(l) = &lang { r.lang = Some(l.clone()); } @@ -416,6 +427,53 @@ fn language(s: &mut Session, p: &Value) -> Result { } }) } +fn language(s: &mut Session, p: &Value) -> Result { + let f = language_edit("format.language", p)?; + format_run(s, &f) +} +/// Every run in the deck, and the text styles new text inherits from, in one language. +fn language_all(s: &mut Session, p: &Value) -> Result { + let f = language_edit("format.languageAll", p)?; + fn shapes(list: &mut [Shape], f: &dyn Fn(&mut RunProps), depth: usize) { + if depth > 64 { + return; + } + for sh in list { + if let Some(t) = &mut sh.text { + ed::format_all(t, f); + } + match &mut sh.kind { + deckcraft_model::ShapeKind::Table(t) => t.rows.iter_mut().flat_map(|r| &mut r.cells).for_each(|c| ed::format_all(&mut c.text, f)), + deckcraft_model::ShapeKind::Group { children, .. } => shapes(children, f, depth + 1), + _ => {} + } + } + } + fn styles(l: &mut deckcraft_model::text::ListStyle, f: &dyn Fn(&mut RunProps)) { + // Level 1 always, so a deck without a default text style gets one. + if l.level(0).is_none() { + l.set(0, Default::default()); + } + l.levels.iter_mut().flatten().for_each(|lv| f(&mut lv.run)); + } + s.edit(|doc, _| { + for sl in &mut doc.slides { + let sl = Arc::make_mut(sl); + shapes(&mut sl.shapes, &f, 0); + ed::format_all(&mut sl.notes, &f); + } + let extra = [doc.notes_master.as_mut(), doc.handout_master.as_mut()]; + for m in doc.masters.iter_mut().chain(extra.into_iter().flatten()) { + let m = Arc::make_mut(m); + shapes(&mut m.shapes, &f, 0); + m.layouts.iter_mut().for_each(|l| shapes(&mut l.shapes, &f, 0)); + [&mut m.title_style, &mut m.body_style, &mut m.other_style].into_iter().for_each(|l| styles(l, &f)); + } + styles(&mut doc.default_text_style, &f); + Ok(()) + })?; + ok() +} fn highlight(s: &mut Session, p: &Value) -> Result { let c = color_param(p, "color"); format_run(s, &move |r| r.highlight = c.clone()) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index fc5cd2c9..d28e2a25 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3756,6 +3756,44 @@ fn language_marks_text_and_no_proof_and_repeats_are_reported() { assert!(s.execute("format.language", &json!({"lang": "en US!"})).is_err()); } +#[test] +fn language_all_marks_every_text_in_the_deck_with_nothing_selected() { + crate::spell::install("xx-AL", crate::spell::Dictionary::parse(b"", b"2\nhello\nworld\n")); + let mut s = session(); + let a = + s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 300, 100], "text": "hello wurld"})).unwrap()["id"].as_u64().unwrap(); + s.execute("edit.select", &json!({"ids": [a]})).unwrap(); + s.execute("format.language", &json!({"lang": "de-DE"})).unwrap(); + s.execute("slide.new", &json!({"title": "world hellow"})).unwrap(); + s.execute("insert.table", &json!({"rows": 1, "cols": 1})).unwrap(); + s.execute("edit.select", &json!({"ids": []})).unwrap(); + s.execute("format.languageAll", &json!({"lang": "xx-AL"})).unwrap(); + let st = s.doc().unwrap(); + let mut langs = vec![]; + for sl in &st.doc.slides { + deckcraft_model::walk(&sl.shapes, &mut |sh, _| { + langs.extend(sh.text.iter().flat_map(|t| &t.paragraphs).flat_map(|p| &p.runs).map(|r| r.props.lang.clone())); + if let deckcraft_model::ShapeKind::Table(t) = &sh.kind { + langs.extend(t.rows.iter().flat_map(|r| &r.cells).flat_map(|c| &c.text.paragraphs).map(|p| p.end_props.lang.clone())); + } + }); + } + assert!(!langs.is_empty() && langs.iter().all(|l| l.as_deref() == Some("xx-AL")), "{langs:?}"); + // The defaults new text inherits. + assert_eq!(st.doc.default_text_style.level(0).unwrap().run.lang.as_deref(), Some("xx-AL")); + assert!(st.doc.masters.iter().all(|m| m.body_style.level(0).is_some_and(|l| l.run.lang.as_deref() == Some("xx-AL")))); + let words: Vec = + s.execute("review.spelling", &json!({})).unwrap().as_array().unwrap().iter().map(|w| w["word"].as_str().unwrap().to_string()).collect(); + assert_eq!(words, vec!["wurld", "hellow"]); + // One undo step puts the per-box language back. + s.execute("edit.undo", &json!({})).unwrap(); + let st = s.doc().unwrap(); + let sh = st.doc.slides.iter().find_map(|sl| deckcraft_model::find_shape(&sl.shapes, deckcraft_model::ShapeId(a as u32))).unwrap(); + assert_eq!(sh.text.as_ref().unwrap().paragraphs[0].runs[0].props.lang.as_deref(), Some("de-DE")); + assert!(s.execute("format.languageAll", &json!({})).is_err()); + assert!(s.execute("format.languageAll", &json!({"lang": "en US!"})).is_err()); +} + #[test] fn the_caret_takes_the_colour_of_the_text() { let caret = |theme: &str| { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index e0fa02e9..2a6dd95d 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2031,7 +2031,21 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let (cur, np) = selected_language(app); let lang = d.get("lang", &cur); let mut no_proof = d.get("noProof", if np { "1" } else { "" }) == "1"; - ui.label("Mark selected text as:"); + // The selected text, or every text in the presentation (all there is with nothing selected). + let has_sel = deckcraft_engine::cmd::has_text_or_shapes(&app.session).is_ok(); + let mut all = !has_sel || d.get("scope", "") == "all"; + ui.horizontal(|ui| { + ui.label("Apply to:"); + if ui.add_enabled(has_sel, egui::RadioButton::new(!all, "Selected text")).clicked() { + d.fields.insert("scope".into(), "sel".into()); + all = false; + } + if ui.radio(all, "Whole presentation").clicked() { + d.fields.insert("scope".into(), "all".into()); + all = true; + } + }); + ui.label(if all { "Mark all text in the presentation as:" } else { "Mark selected text as:" }); egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| { // The selection's own language first when the list doesn't have it. let own = (!crate::squiggles::LANGS.iter().any(|(_, t)| t.eq_ignore_ascii_case(&cur))).then(|| (cur.clone(), cur.clone())); @@ -2052,7 +2066,7 @@ fn body(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let (ok, cancel) = buttons(ui, "OK"); if ok { let lang = d.get("lang", &cur); - run(app, "format.language", json!({"lang": lang, "noProof": no_proof})); + run(app, if all { "format.languageAll" } else { "format.language" }, json!({"lang": lang, "noProof": no_proof})); } ok || cancel } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 2c7fd8cc..c121c141 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -3144,6 +3144,41 @@ mod tests { assert_eq!(body.paragraphs[0].runs[0].props.lang.as_deref(), Some("fr-FR")); } + /// Review > Language > Whole presentation marks every text box, not only the selected one. + #[test] + fn the_language_dialog_marks_the_whole_presentation() { + use egui_kittest::kittest::Queryable; + let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(Session::with_new(), Services::default()), + ); + harness.run_steps(2); + let s = &mut harness.state_mut().session; + let title = s.active().unwrap().doc.slides[0].shapes[0].id.0; + s.execute("text.edit", &json!({"id": title})).unwrap(); + s.execute("text.insert", &json!({"text": "Hola"})).unwrap(); + s.execute("slide.new", &json!({"title": "Adiós"})).unwrap(); + s.execute("text.edit", &json!({"id": s.active().unwrap().doc.slides[1].shapes[0].id.0})).unwrap(); + s.execute("text.selectAll", &json!({})).unwrap(); + harness.state_mut().run("app.dialog", json!({"id": "language"})).unwrap(); + harness.run_steps(4); + harness.get_by_label("Whole presentation").click(); + harness.run_steps(2); + harness.get_by_label("Español (España)").click(); + harness.run_steps(2); + harness.get_by_label("OK").click(); + harness.run_steps(2); + assert!(harness.state().dialog.is_none()); + let d = harness.state().session.active().unwrap(); + for sl in &d.doc.slides { + let body = sl.shapes[0].text.as_ref().unwrap(); + assert_eq!(body.paragraphs[0].runs[0].props.lang.as_deref(), Some("es-ES")); + } + } + /// Speaker notes keep their formatting when edited in the notes pane: typing into a bold /// heading, Enter in a bulleted point, then saving as .pptx and reopening. #[test] diff --git a/docs/gaps.md b/docs/gaps.md index 58f5780c..b5f09f56 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -256,8 +256,9 @@ and starts at that effect. The pane has an advanced timeline with draggable bars for Add to Dictionary. Red squiggles on the current slide come from a worker thread, with a context menu to correct, ignore or add; table cells and speaker notes are squiggled too, each with a right-click menu; repeated words (`the the`) are flagged with Delete Repeated Word; - Review › Language marks the selection's proofing language or Do Not Check Spelling - (`lang`/`noProof` round-trip in PPTX). No grammar checking. The web build checks on the UI + Review › Language marks the selection's proofing language or Do Not Check Spelling, or the + whole presentation's (`format.languageAll`: every run, notes, masters, layouts and the default + text style) (`lang`/`noProof` round-trip in PPTX). No grammar checking. The web build checks on the UI thread a few milliseconds per frame. No dictionaries ship with DeckCraft, so without one installed (always on Windows and the web) only a short list of common misspellings is found. `review.thesaurus` looks up the selected word (checklist row partial). AutoCorrect has diff --git a/docs/localization-parity.md b/docs/localization-parity.md index f1cf8047..1128dc45 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -1,6 +1,6 @@ # Localization parity with PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (IME composition underlined in table cells; previous: minor, IME composition and CJK interface text, #101; previous: major, created) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-21 · **Change:** minor (one proofing language for the whole presentation; previous: minor, IME composition underlined in table cells; previous: minor, IME composition and CJK interface text, #101; previous: major, created) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft's interface is English only today: strings are literals in the UI and engine code, with no catalog and no language switch. Requests: #15 (follow the system language), #30 and PR #34 @@ -53,11 +53,14 @@ the next re-measure. PowerPoint checks spelling in dozens of languages and lets each run carry a proofing language. DeckCraft keeps `a:rPr/@lang` on round trip but its spelling check is a small English list of common misspellings (`crates/engine/src/cmd/review.rs`); see [gaps.md](gaps.md#proofing-spelling-thesaurus-autocorrect). +Review › Language sets the proofing language of the selected text or, with *Whole presentation*, +of every text in the file and the default for new text (`format.languageAll`). ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-10-21 | minor | Review › Language can set the proofing language of the whole presentation | | 2026-10-14 | minor | IME composition in a table cell is underlined in place; the candidate window sits on that cell's caret | | 2026-10-12 | minor | IME composition for Chinese, Japanese and Korean, and interface text in any installed script (#101) | | 2026-10-10 | major | Created: twelve-language table, ~1,050 UI strings measured, PowerPoint for Mac's 26 languages from its bundle |