From 6a1b8f1e39aa2569e266e7d44e63ef358d3abe37 Mon Sep 17 00:00:00 2001 From: Zach Berry <1112351+zberry@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:20:24 -0500 Subject: [PATCH] [ZZ1] insert.dateTime in the Handout/Notes Master view fills the master's date placeholder PowerPoint: Header and Footer > Notes and Handouts > Date and time puts a date field in the date placeholder of the notes and handout masters, and every handout and notes page shows it. Before, with no text caret the command wrote the current slide's date placeholder (design .headerFooter) even in those views, so the handout PDF showed nothing. Now it writes the selected date placeholder, else the master's own, as a field (update) or plain text, answers where: master, and refuses (changing nothing) when the master has no date placeholder. Co-Authored-By: Claude Fable 5.1 --- crates/engine/src/cmd/insert.rs | 66 ++++++++++++++++++++- crates/engine/src/cmd/insert/text_fields.rs | 48 +++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index 76bc5359..4c20c00e 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -98,7 +98,7 @@ pub fn specs() -> Vec { "Date & Time", ["Insert", "Text"], None, - "{format?: datetime1..datetime13, update?: bool, language?: en-US|en-GB|fr-FR|ja-JP|sv-SE} — no keys opens the dialog", + "{format?: datetime1..datetime13, update?: bool, language?: en-US|en-GB|fr-FR|ja-JP|sv-SE} — no keys opens the dialog; in the Handout or Notes Master view it fills the selected date placeholder (else the master's own) and answers where: \"master\"", has_slide, date_time ), @@ -473,6 +473,70 @@ mod tests { } } + /// ZZ1: in the handout and notes master views the date goes into the master's date placeholder + /// (not the current slide), so the exported handout and notes pages show it. + #[test] + fn on_a_page_master_the_date_fills_its_date_placeholder() { + for view in ["view.handoutMaster", "view.notesMaster"] { + let mut s = session(); + s.execute(view, &json!({})).unwrap(); + let (id, master) = { + let st = s.doc().unwrap(); + let m = crate::page_master(&st.doc, st.selection.target).unwrap().clone(); + (m.shapes.iter().find(|x| x.ph_type() == Some(deckcraft_model::PhType::Date)).unwrap().id.0, m) + }; + assert!(master.shapes.iter().find(|x| x.id.0 == id).and_then(|x| x.text.as_ref()).is_none_or(|t| t.text().is_empty())); + s.execute("edit.select", &json!({"ids": [id]})).unwrap(); + let v = s.execute("insert.dateTime", &json!({"format": "datetime10", "update": true})).unwrap(); + assert_eq!(v["where"], "master", "{view}: {v}"); + assert!(date_run(&s).is_none(), "the slide keeps no date"); + let st = s.doc().unwrap(); + let m = crate::page_master(&st.doc, st.selection.target).unwrap(); + let run = m + .shapes + .iter() + .find(|x| x.id.0 == id) + .and_then(|x| x.text.as_ref()) + .and_then(|t| t.paragraphs.first()) + .and_then(|p| p.runs.first()) + .cloned() + .unwrap(); + assert!(matches!(run.kind, RunKind::Field { ref field } if field == "datetime10"), "{view}: {run:?}"); + let placed = deckcraft_render::place_master_texts(&st.doc, m, 1); + assert!(placed.iter().any(|p| p.body.text() == run.text && run.text.contains(':')), "{view}: the page shows the date"); + s.execute("edit.undo", &json!({})).unwrap(); + let st = s.doc().unwrap(); + let m = crate::page_master(&st.doc, st.selection.target).unwrap(); + assert!(m.shapes.iter().find(|x| x.id.0 == id).and_then(|x| x.text.as_ref()).is_none_or(|t| t.text().is_empty()), "one undo"); + // A fixed date in a placeholder whose run is en-GB reads day first, and the answer + // reports the text that was written, not the en-US sample. + s.execute("insert.dateTime", &json!({"format": "datetime1", "update": false, "language": "en-GB"})).unwrap(); + let v = s.execute("insert.dateTime", &json!({"format": "datetime1", "update": false})).unwrap(); + let st = s.doc().unwrap(); + let m = crate::page_master(&st.doc, st.selection.target).unwrap(); + let run = m + .shapes + .iter() + .find(|x| x.id.0 == id) + .and_then(|x| x.text.as_ref()) + .and_then(|t| t.paragraphs.first()) + .and_then(|p| p.runs.first()) + .cloned() + .unwrap(); + assert_eq!(run.props.lang.as_deref(), Some("en-GB"), "{view}: {run:?}"); + assert_eq!(v["text"].as_str(), Some(run.text.as_str()), "{view}: the answer reports the written text: {v}"); + let now = deckcraft_text::datetime::DateTime::now_local().unwrap(); + let gb = deckcraft_text::datetime::field_text("datetime1", Some("en-GB"), now); + assert_eq!(Some(run.text.as_str()), gb.as_deref(), "{view}: the date in en-GB order (dd/mm/yyyy)"); + // No date placeholder: refused, nothing changed. + let key = if view == "view.handoutMaster" { "handout.placeholders" } else { "notes.placeholders" }; + s.execute(key, &json!({"date": false})).unwrap(); + let rev = s.doc().unwrap().revision; + assert!(s.execute("insert.dateTime", &json!({"format": "datetime1"})).is_err()); + assert_eq!(s.doc().unwrap().revision, rev); + } + } + #[test] fn without_a_caret_the_date_lands_on_this_slide_only() { let mut s = session(); diff --git a/crates/engine/src/cmd/insert/text_fields.rs b/crates/engine/src/cmd/insert/text_fields.rs index d19f1dd1..ebf7c62f 100644 --- a/crates/engine/src/cmd/insert/text_fields.rs +++ b/crates/engine/src/cmd/insert/text_fields.rs @@ -149,6 +149,11 @@ fn apply_date(s: &mut Session, o: &DateOpts) -> Result { } return Ok(json!({"where": "caret", "format": o.format, "update": o.update, "text": o.sample})); } + // Handout or Notes Master view: the master's date placeholder (the selected one, else the + // master's own), which every handout or notes page then shows. + if s.doc()?.selection.target.is_page_master() { + return write_master_date(s, o); + } // Not editing: the slide's date placeholder (this slide only). A layout with no date // placeholder gets a text box instead, so the chosen format is still inserted. s.execute("design.headerFooter", &json!({"date": true, "all": false}))?; @@ -158,6 +163,49 @@ fn apply_date(s: &mut Session, o: &DateOpts) -> Result { Ok(json!({"where": "footer", "format": o.format, "update": o.update, "text": o.sample})) } +/// Put the date in the date placeholder of the handout or notes master being edited. +fn write_master_date(s: &mut Session, o: &DateOpts) -> Result { + let (format, sample, lang, update) = (o.format.clone(), o.sample.clone(), o.lang.clone(), o.update); + let has_date = { + let st = s.doc()?; + crate::page_master(&st.doc, st.selection.target).is_some_and(|m| m.shapes.iter().any(|x| x.ph_type() == Some(deckcraft_model::PhType::Date))) + }; + if !has_date { + return Err(bad( + "insert.dateTime", + "this master has no date placeholder; show it with handout.placeholders or notes.placeholders {date: true}", + )); + } + // The text the master holds afterwards, which the answer reports (it can differ from the + // caller's sample when the placeholder's own language formats a fixed date). + let mut written = o.sample.clone(); + s.edit(|doc, sel| { + let Some(m) = crate::page_master_mut(doc, sel.target) else { return Ok(false) }; + let m = std::sync::Arc::make_mut(m); + let date = |x: &deckcraft_model::Shape| x.ph_type() == Some(deckcraft_model::PhType::Date); + let chosen = sel.shapes.iter().find_map(|id| m.shapes.iter().position(|x| x.id == *id && date(x))); + let Some(i) = chosen.or_else(|| m.shapes.iter().position(date)) else { return Ok(false) }; + let Some(sh) = m.shapes.get_mut(i) else { return Ok(false) }; + let mut props = sh.text.as_ref().and_then(|t| t.paragraphs.first()).and_then(|p| p.runs.first()).map(|r| r.props.clone()).unwrap_or_default(); + // A fixed date reads in the placeholder's own language when the caller named none. + let text = match (&lang, &props.lang) { + (None, Some(l)) if !update => date_sample(&format, Some(l)).unwrap_or_else(|_| sample.clone()), + _ => sample.clone(), + }; + if let Some(lang) = lang { + props.lang = Some(lang); + } + let kind = if update { RunKind::Field { field: format } } else { RunKind::Text }; + let body = sh.text.clone().unwrap_or_default(); + let mut para = body.paragraphs.first().cloned().unwrap_or_default(); + written.clone_from(&text); + para.runs = vec![deckcraft_model::Run { text, props, kind }]; + sh.text = Some(TextBody { paragraphs: vec![para], ..body }); + Ok(true) + })?; + Ok(json!({"where": "master", "format": o.format, "update": o.update, "text": written})) +} + /// Point the current slide's date placeholder at this format (a field, or the sample as plain text). fn write_date_placeholder(s: &mut Session, o: &DateOpts) -> Result { let format = o.format.clone();