mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Add the Notes Master view
View ▸ Notes Master edits the notes master on a notes-size page (creating PowerPoint's default placeholders if the deck has none); its tab toggles header, date, slide image, body, footer and page number, sets the background and closes the view. The PPTX writer now writes an edited notes master as it is, even in a deck without notes. Stacked on #572.
This commit is contained in:
@@ -65,6 +65,24 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
in_handout,
|
||||
handout_placeholders
|
||||
),
|
||||
cmd!(
|
||||
"view.notesMaster",
|
||||
"Notes Master",
|
||||
["View", "Master Views"],
|
||||
None,
|
||||
"{} (creates the notes master with header, date, slide image, notes body, footer and page number if the deck has none)",
|
||||
has_doc,
|
||||
notes_view
|
||||
),
|
||||
cmd!(
|
||||
"notes.placeholders",
|
||||
"Notes Placeholders",
|
||||
["Notes Master", "Placeholders"],
|
||||
None,
|
||||
"{header?, date?, slideImage?, body?, footer?, pageNumber?: bool} (show or hide each)",
|
||||
in_notes_master,
|
||||
notes_placeholders
|
||||
),
|
||||
cmd!(noundo "view.closeMaster", "Close Master View", ["Slide Master", "Close"], None, "{}", has_doc, close_master),
|
||||
cmd!("master.insertLayout", "Insert Layout", ["Slide Master", "Edit Master"], None, "{name?}", has_doc, insert_layout),
|
||||
cmd!("master.renameLayout", "Rename Layout", ["Slide Master", "Edit Master"], None, "{name, master?, layout?}", has_doc, rename_layout),
|
||||
@@ -100,7 +118,7 @@ fn theme(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn master_index(doc: &Presentation, sel: &crate::Selection) -> usize {
|
||||
match sel.target {
|
||||
Target::Master { master } | Target::Layout { master, .. } => master,
|
||||
Target::Slides | Target::Handout => {
|
||||
Target::Slides | Target::Handout | Target::NotesMaster => {
|
||||
doc.slides.get(sel.slide).and_then(|s| doc.masters.iter().position(|m| m.layout(s.layout).is_some())).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
@@ -216,8 +234,8 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
l.background = bg.clone();
|
||||
}
|
||||
}
|
||||
Target::Handout => {
|
||||
if let Some(m) = doc.handout_master.as_mut() {
|
||||
t @ (Target::Handout | Target::NotesMaster) => {
|
||||
if let Some(m) = crate::page_master_mut(doc, t) {
|
||||
Arc::make_mut(m).background = bg.clone();
|
||||
}
|
||||
}
|
||||
@@ -554,6 +572,110 @@ fn handout_placeholders(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"header": shown(PhType::Header), "date": shown(PhType::Date), "footer": shown(PhType::Footer), "pageNumber": shown(PhType::SlideNum)}))
|
||||
}
|
||||
|
||||
fn in_notes_master(s: &Session) -> std::result::Result<(), String> {
|
||||
if s.active().is_some_and(|d| d.selection.target == Target::NotesMaster) { Ok(()) } else { Err("open View ▸ Notes Master first".into()) }
|
||||
}
|
||||
|
||||
const NOTES_KINDS: [(&str, PhType); 6] = [
|
||||
("header", PhType::Header),
|
||||
("date", PhType::Date),
|
||||
("slideImage", PhType::SlideImage),
|
||||
("body", PhType::Body),
|
||||
("footer", PhType::Footer),
|
||||
("pageNumber", PhType::SlideNum),
|
||||
];
|
||||
|
||||
/// A notes-master placeholder in a page of `size`, laid out as PowerPoint's default notes master.
|
||||
pub fn notes_placeholder(kind: PhType, size: Size, id: ShapeId) -> Shape {
|
||||
let (w, h) = (size.width, size.height);
|
||||
match kind {
|
||||
PhType::SlideImage => Shape {
|
||||
id,
|
||||
name: "Slide Image Placeholder 3".into(),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(w * 0.125, h * 0.125, w * 0.75, h * 0.375)),
|
||||
ph: Some(deckcraft_model::Placeholder { kind, idx: 2, ..Default::default() }),
|
||||
fill: Some(Fill::None),
|
||||
line: Some(deckcraft_model::Line::solid(deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::BLACK), 1.0)),
|
||||
..Default::default()
|
||||
},
|
||||
PhType::Body => {
|
||||
let mut text = TextBody::default();
|
||||
text.paragraphs = vec![deckcraft_model::text::Paragraph::new("Click to edit Master text styles")];
|
||||
Shape {
|
||||
id,
|
||||
name: "Notes Placeholder 4".into(),
|
||||
xfrm: Some(deckcraft_geom::Xfrm::new(w * 0.1, h * 0.53, w * 0.8, h * 0.36)),
|
||||
ph: Some(deckcraft_model::Placeholder { kind, idx: 3, size: Some("quarter".into()), ..Default::default() }),
|
||||
text: Some(text),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut s = handout_placeholder(kind, size, id);
|
||||
if let Some(ph) = s.ph.as_mut() {
|
||||
ph.idx = match kind {
|
||||
PhType::Header => 0,
|
||||
PhType::Date => 1,
|
||||
PhType::Footer => 4,
|
||||
_ => 5,
|
||||
};
|
||||
}
|
||||
s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notes_view(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
let created = s.doc()?.doc.notes_master.as_ref().is_none_or(|m| m.shapes.is_empty());
|
||||
if created {
|
||||
s.edit(|doc, _| {
|
||||
let mut m = doc.notes_master.as_deref().cloned().unwrap_or_else(|| deckcraft_model::Master {
|
||||
name: "Notes Master".into(),
|
||||
theme: doc.masters.first().map(|m| m.theme.clone()).unwrap_or_default(),
|
||||
color_map: doc.masters.first().map(|m| m.color_map.clone()).unwrap_or_default(),
|
||||
..Default::default()
|
||||
});
|
||||
let size = doc.notes_size;
|
||||
for (_, k) in NOTES_KINDS {
|
||||
let id = ShapeId(doc.alloc_id());
|
||||
m.shapes.push(notes_placeholder(k, size, id));
|
||||
}
|
||||
doc.notes_master = Some(Arc::new(m));
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
s.select(|_, sel| {
|
||||
sel.target = Target::NotesMaster;
|
||||
sel.shapes.clear();
|
||||
sel.text = None;
|
||||
})?;
|
||||
Ok(json!({"created": created}))
|
||||
}
|
||||
|
||||
fn notes_placeholders(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit(|doc, _| {
|
||||
let size = doc.notes_size;
|
||||
let ids: Vec<ShapeId> = NOTES_KINDS.iter().map(|_| ShapeId(doc.alloc_id())).collect();
|
||||
let m = Arc::make_mut(doc.notes_master.as_mut().ok_or_else(|| bad("notes.placeholders", "no notes master"))?);
|
||||
for ((key, kind), id) in NOTES_KINDS.into_iter().zip(ids) {
|
||||
let Some(on) = p.get(key).and_then(Value::as_bool) else { continue };
|
||||
let has = m.shapes.iter().any(|x| x.ph_type() == Some(kind));
|
||||
if on && !has {
|
||||
m.shapes.push(notes_placeholder(kind, size, id));
|
||||
} else if !on {
|
||||
m.shapes.retain(|x| x.ph_type() != Some(kind));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
let m = s.doc()?.doc.notes_master.clone();
|
||||
let mut out = serde_json::Map::new();
|
||||
for (key, kind) in NOTES_KINDS {
|
||||
out.insert(key.into(), json!(m.as_ref().is_some_and(|m| m.shapes.iter().any(|x| x.ph_type() == Some(kind)))));
|
||||
}
|
||||
Ok(Value::Object(out))
|
||||
}
|
||||
|
||||
fn close_master(s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
s.select(|_, sel| {
|
||||
sel.target = Target::Slides;
|
||||
|
||||
@@ -274,7 +274,9 @@ fn ctx_xfrm(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape:
|
||||
match sel.target {
|
||||
crate::Target::Slides => doc.slides.get(sel.slide).and_then(|s| Ctx::for_slide(doc, s)).map(|c| resolve::xfrm(&c, shape)),
|
||||
crate::Target::Master { master } => doc.masters.get(master).map(|m| resolve::xfrm(&Ctx::for_master(doc, m), shape)),
|
||||
crate::Target::Handout => doc.handout_master.as_ref().map(|m| resolve::xfrm(&Ctx::for_master(doc, m), shape)),
|
||||
t @ (crate::Target::Handout | crate::Target::NotesMaster) => {
|
||||
crate::page_master(doc, t).map(|m| resolve::xfrm(&Ctx::for_master(doc, m), shape))
|
||||
}
|
||||
crate::Target::Layout { master, layout } => {
|
||||
doc.masters.get(master).and_then(|m| m.layouts.get(layout).map(|l| resolve::xfrm(&Ctx::for_layout(doc, m, l), shape)))
|
||||
}
|
||||
@@ -328,7 +330,7 @@ pub(crate) fn current_shapes<'a>(doc: &'a deckcraft_model::Presentation, sel: &c
|
||||
match sel.target {
|
||||
crate::Target::Slides => doc.slides.get(sel.slide).map(|s| &s.shapes),
|
||||
crate::Target::Master { master } => doc.masters.get(master).map(|m| &m.shapes),
|
||||
crate::Target::Handout => doc.handout_master.as_ref().map(|m| &m.shapes),
|
||||
t @ (crate::Target::Handout | crate::Target::NotesMaster) => crate::page_master(doc, t).map(|m| &m.shapes),
|
||||
crate::Target::Layout { master, layout } => doc.masters.get(master).and_then(|m| m.layouts.get(layout)).map(|l| &l.shapes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ pub(crate) fn ctx_for<'a>(doc: &'a Presentation, sel: &Selection) -> Option<Ctx<
|
||||
match sel.target {
|
||||
Target::Slides => doc.slides.get(sel.slide).and_then(|s| Ctx::for_slide(doc, s)),
|
||||
Target::Master { master } => doc.masters.get(master).map(|m| Ctx::for_master(doc, m)),
|
||||
Target::Handout => doc.handout_master.as_ref().map(|m| Ctx::for_master(doc, m)),
|
||||
t @ (Target::Handout | Target::NotesMaster) => crate::page_master(doc, t).map(|m| Ctx::for_master(doc, m)),
|
||||
Target::Layout { master, layout } => doc.masters.get(master).and_then(|m| m.layouts.get(layout).map(|l| Ctx::for_layout(doc, m, l))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,6 +103,32 @@ pub enum Target {
|
||||
},
|
||||
/// The handout master (View ▸ Handout Master), a page at the notes size.
|
||||
Handout,
|
||||
/// The notes master (View ▸ Notes Master), a page at the notes size.
|
||||
NotesMaster,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
/// A notes-size master page (Handout or Notes Master view).
|
||||
pub fn is_page_master(self) -> bool {
|
||||
matches!(self, Target::Handout | Target::NotesMaster)
|
||||
}
|
||||
}
|
||||
|
||||
/// The handout or notes master edited in that view.
|
||||
pub fn page_master(doc: &Presentation, t: Target) -> Option<&Arc<deckcraft_model::Master>> {
|
||||
match t {
|
||||
Target::Handout => doc.handout_master.as_ref(),
|
||||
Target::NotesMaster => doc.notes_master.as_ref(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_master_mut(doc: &mut Presentation, t: Target) -> Option<&mut Arc<deckcraft_model::Master>> {
|
||||
match t {
|
||||
Target::Handout => doc.handout_master.as_mut(),
|
||||
Target::NotesMaster => doc.notes_master.as_mut(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
@@ -215,7 +241,7 @@ impl DocState {
|
||||
match self.selection.target {
|
||||
Target::Slides => self.current_slide().map(|s| s.shapes.as_slice()).unwrap_or(&[]),
|
||||
Target::Master { master } => self.doc.masters.get(master).map(|m| m.shapes.as_slice()).unwrap_or(&[]),
|
||||
Target::Handout => self.doc.handout_master.as_ref().map(|m| m.shapes.as_slice()).unwrap_or(&[]),
|
||||
Target::Handout | Target::NotesMaster => page_master(&self.doc, self.selection.target).map(|m| m.shapes.as_slice()).unwrap_or(&[]),
|
||||
Target::Layout { master, layout } => {
|
||||
self.doc.masters.get(master).and_then(|m| m.layouts.get(layout)).map(|l| l.shapes.as_slice()).unwrap_or(&[])
|
||||
}
|
||||
@@ -231,13 +257,13 @@ impl DocState {
|
||||
|
||||
/// The page being edited: the slide size, or the notes size in Handout Master view.
|
||||
pub fn page_size(st: &DocState) -> deckcraft_geom::Size {
|
||||
if st.selection.target == Target::Handout { st.doc.notes_size } else { st.doc.slide_size }
|
||||
if st.selection.target.is_page_master() { st.doc.notes_size } else { st.doc.slide_size }
|
||||
}
|
||||
|
||||
/// The handout master as a one-master presentation at the handout page size, for drawing it with
|
||||
/// the layout renderer.
|
||||
pub fn handout_page(doc: &Presentation) -> Option<Presentation> {
|
||||
let m = doc.handout_master.clone()?;
|
||||
/// The handout or notes master as a one-master presentation at the notes page size, for drawing
|
||||
/// it with the layout renderer.
|
||||
pub fn master_page(doc: &Presentation, t: Target) -> Option<Presentation> {
|
||||
let m = page_master(doc, t)?.clone();
|
||||
let mut p = doc.clone();
|
||||
p.slide_size = doc.notes_size;
|
||||
p.masters = vec![m];
|
||||
@@ -250,7 +276,7 @@ pub fn shapes_mut<'a>(doc: &'a mut Presentation, sel: &Selection) -> Option<&'a
|
||||
match sel.target {
|
||||
Target::Slides => doc.slides.get_mut(sel.slide).map(|s| &mut Arc::make_mut(s).shapes),
|
||||
Target::Master { master } => doc.masters.get_mut(master).map(|m| &mut Arc::make_mut(m).shapes),
|
||||
Target::Handout => doc.handout_master.as_mut().map(|m| &mut Arc::make_mut(m).shapes),
|
||||
t @ (Target::Handout | Target::NotesMaster) => page_master_mut(doc, t).map(|m| &mut Arc::make_mut(m).shapes),
|
||||
Target::Layout { master, layout } => {
|
||||
doc.masters.get_mut(master).and_then(|m| Arc::make_mut(m).layouts.get_mut(layout)).map(|l| &mut l.shapes)
|
||||
}
|
||||
@@ -557,7 +583,7 @@ fn clamp_selection(st: &mut DocState) {
|
||||
st.selection.slides = slides;
|
||||
match st.selection.target {
|
||||
Target::Master { master } if master >= st.doc.masters.len() => st.selection.target = Target::Slides,
|
||||
Target::Handout if st.doc.handout_master.is_none() => st.selection.target = Target::Slides,
|
||||
t @ (Target::Handout | Target::NotesMaster) if page_master(&st.doc, t).is_none() => st.selection.target = Target::Slides,
|
||||
Target::Layout { master, layout } if st.doc.masters.get(master).is_none_or(|m| layout >= m.layouts.len()) => {
|
||||
st.selection.target = Target::Slides
|
||||
}
|
||||
|
||||
@@ -862,7 +862,7 @@ fn handout_master_view_edits_and_round_trips() {
|
||||
s.execute("shape.insert", &json!({"preset": "rect", "rect": [20, 300, 100, 40]})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.handout_master.as_ref().unwrap().shapes.len(), 4);
|
||||
assert_eq!(s.doc().unwrap().doc.slides[0].shapes.len(), slide_shapes);
|
||||
assert!(crate::handout_page(&s.doc().unwrap().doc).is_some_and(|p| p.masters.len() == 1 && p.slide_size == p.notes_size));
|
||||
assert!(crate::master_page(&s.doc().unwrap().doc, Target::Handout).is_some_and(|p| p.masters.len() == 1 && p.slide_size == p.notes_size));
|
||||
s.execute("view.closeMaster", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.target, Target::Slides);
|
||||
// Reopening doesn't make a second master, and it survives PPTX.
|
||||
@@ -873,3 +873,33 @@ fn handout_master_view_edits_and_round_trips() {
|
||||
assert_eq!(hm.shapes.len(), 4);
|
||||
assert!(hm.shapes.iter().any(|x| x.ph_type() == Some(PhType::SlideNum)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notes_master_view_edits_and_saves() {
|
||||
use deckcraft_model::PhType;
|
||||
let mut s = Session::with_new();
|
||||
assert!(s.execute("notes.placeholders", &json!({"header": false})).is_err(), "only in the view");
|
||||
assert_eq!(s.execute("view.notesMaster", &json!({})).unwrap()["created"], true);
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.selection.target, Target::NotesMaster);
|
||||
assert_eq!(crate::page_size(st), st.doc.notes_size);
|
||||
let kinds: Vec<_> = st.shapes().iter().filter_map(|x| x.ph_type()).collect();
|
||||
assert_eq!(kinds, [PhType::Header, PhType::Date, PhType::SlideImage, PhType::Body, PhType::Footer, PhType::SlideNum]);
|
||||
let r = s.execute("notes.placeholders", &json!({"header": false, "date": false})).unwrap();
|
||||
assert_eq!((r["header"].as_bool(), r["body"].as_bool()), (Some(false), Some(true)));
|
||||
assert_eq!(s.doc().unwrap().shapes().len(), 4);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().shapes().len(), 6);
|
||||
s.execute("notes.placeholders", &json!({"header": false})).unwrap();
|
||||
// A shape added in the view is on the notes master and is saved, even with no notes in the deck.
|
||||
s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [20, 20, 60, 60]})).unwrap();
|
||||
assert!(s.doc().unwrap().doc.slides.iter().all(|sl| sl.notes.is_empty()));
|
||||
assert!(crate::master_page(&s.doc().unwrap().doc, Target::NotesMaster).is_some());
|
||||
let bytes = deckcraft_pptx::export(&s.doc().unwrap().doc).unwrap();
|
||||
let back = deckcraft_pptx::import(&bytes).unwrap();
|
||||
let nm = back.notes_master.expect("notes master written");
|
||||
assert_eq!(nm.shapes.len(), 6, "{:?}", nm.shapes.iter().map(|x| &x.name).collect::<Vec<_>>());
|
||||
assert!(!nm.shapes.iter().any(|x| x.ph_type() == Some(PhType::Header)));
|
||||
s.execute("view.closeMaster", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("view.notesMaster", &json!({})).unwrap()["created"], false);
|
||||
}
|
||||
|
||||
@@ -439,7 +439,9 @@ pub fn export(p: &Presentation) -> Result<Vec<u8>, PptxError> {
|
||||
let has_notes = p.slides.iter().any(|s| !s.notes.is_empty());
|
||||
let notes_master = "ppt/notesMasters/notesMaster1.xml";
|
||||
let mut notes_master_rid = None;
|
||||
if has_notes {
|
||||
// A notes master edited in Notes Master view is written as it is; otherwise the default one.
|
||||
let kept_notes_master = p.notes_master.as_ref().filter(|m| !m.shapes.is_empty());
|
||||
if has_notes || kept_notes_master.is_some() {
|
||||
let tname = format!("ppt/theme/theme{}.xml", p.masters.len() + 1);
|
||||
let mut no = Out::new(notes_master);
|
||||
no.rels.add("theme", &opc::relative(notes_master, &tname), false);
|
||||
@@ -451,8 +453,16 @@ pub fn export(p: &Presentation) -> Result<Vec<u8>, PptxError> {
|
||||
let mut w = W::new();
|
||||
root_open(&mut w, "p:notesMaster", A::new());
|
||||
let bg = Background::Ref { idx: 1001, color: deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1) };
|
||||
csld(&mut w, &mut x, &mut no, "", Some(&bg), ¬es_master_shapes(p.notes_size));
|
||||
clr_map(&mut w, &[]);
|
||||
match kept_notes_master {
|
||||
Some(m) => {
|
||||
csld(&mut w, &mut x, &mut no, "", Some(m.background.as_ref().unwrap_or(&bg)), &m.shapes);
|
||||
clr_map(&mut w, &m.color_map);
|
||||
}
|
||||
None => {
|
||||
csld(&mut w, &mut x, &mut no, "", Some(&bg), ¬es_master_shapes(p.notes_size));
|
||||
clr_map(&mut w, &[]);
|
||||
}
|
||||
}
|
||||
dml::list_style(&mut w, &mut x, &mut no, "p:notesStyle", ¬es_style());
|
||||
w.close("p:notesMaster");
|
||||
x.pkg.xml_part(notes_master, CT_NOTES_MASTER, w.finish());
|
||||
|
||||
@@ -124,7 +124,9 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
Some((Target::Master { master }, d)) => master_tex(ui.ctx(), &d.doc, master, None, px, visible),
|
||||
Some((Target::Layout { master, layout }, d)) => master_tex(ui.ctx(), &d.doc, master, Some(layout), px, visible),
|
||||
Some((Target::Handout, d)) => deckcraft_engine::handout_page(&d.doc).and_then(|p| master_tex(ui.ctx(), &p, 0, None, px, visible)),
|
||||
Some((t @ (Target::Handout | Target::NotesMaster), d)) => {
|
||||
deckcraft_engine::master_page(&d.doc, t).and_then(|p| master_tex(ui.ctx(), &p, 0, None, px, visible))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if let Some((tex, region)) = tex {
|
||||
|
||||
@@ -191,6 +191,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)> {
|
||||
("-", ""),
|
||||
("Slide Master", "view.slideMaster"),
|
||||
("Handout Master", "view.handoutMaster"),
|
||||
("Notes Master", "view.notesMaster"),
|
||||
("-", ""),
|
||||
("Ruler", "view.ruler"),
|
||||
("Gridlines", "view.gridlines"),
|
||||
|
||||
@@ -108,6 +108,8 @@ fn contextual(app: &SlideApp) -> Vec<&'static str> {
|
||||
let Some(st) = app.session.active() else { return v };
|
||||
if st.selection.target == Target::Handout {
|
||||
v.push("Handout Master");
|
||||
} else if st.selection.target == Target::NotesMaster {
|
||||
v.push("Notes Master");
|
||||
} else if st.selection.target != Target::Slides {
|
||||
v.push("Slide Master");
|
||||
}
|
||||
@@ -225,6 +227,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
|
||||
"Playback" => playback(app, ui),
|
||||
"Slide Master" => slide_master(app, ui),
|
||||
"Handout Master" => handout_master(app, ui),
|
||||
"Notes Master" => notes_master(app, ui),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
@@ -1829,8 +1832,10 @@ fn view(app: &mut SlideApp, ui: &mut Ui) {
|
||||
if big_button_ex(ui, Icon::HandoutMaster, "Handout\nMaster", true, false, in_handout).0.clicked() {
|
||||
let _ = app.run(if in_handout { "view.closeMaster" } else { "view.handoutMaster" }, json!({}));
|
||||
}
|
||||
let r = big_button(ui, Icon::NotesMaster, "Notes\nMaster", false);
|
||||
let _ = r.on_hover_text("Notes Master (coming soon)");
|
||||
let in_notes = app.session.active().is_some_and(|d| d.selection.target == Target::NotesMaster);
|
||||
if big_button_ex(ui, Icon::NotesMaster, "Notes\nMaster", true, false, in_notes).0.clicked() {
|
||||
let _ = app.run(if in_notes { "view.closeMaster" } else { "view.notesMaster" }, json!({}));
|
||||
}
|
||||
});
|
||||
group(ui, |ui| {
|
||||
rows(ui, |ui| {
|
||||
@@ -2413,6 +2418,36 @@ fn slide_master(app: &mut SlideApp, ui: &mut Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
fn notes_master(app: &mut SlideApp, ui: &mut Ui) {
|
||||
group(ui, |ui| {
|
||||
use deckcraft_model::PhType;
|
||||
let shown = |app: &SlideApp, k: PhType| {
|
||||
app.session.active().and_then(|d| d.doc.notes_master.as_ref()).is_some_and(|m| m.shapes.iter().any(|s| s.ph_type() == Some(k)))
|
||||
};
|
||||
for col in [
|
||||
[("Header", "header", PhType::Header), ("Slide Image", "slideImage", PhType::SlideImage), ("Footer", "footer", PhType::Footer)],
|
||||
[("Date", "date", PhType::Date), ("Body", "body", PhType::Body), ("Page Number", "pageNumber", PhType::SlideNum)],
|
||||
] {
|
||||
rows(ui, |ui| {
|
||||
for (label, key, k) in col {
|
||||
let mut on = shown(app, k);
|
||||
if ui.checkbox(&mut on, label).changed() {
|
||||
run(app, "notes.placeholders", json!({ key: on }));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
group(ui, |ui| {
|
||||
big(app, ui, Icon::FormatBackground, "Background\nStyles", "view.pane", json!({"pane": "background"}));
|
||||
});
|
||||
group(ui, |ui| {
|
||||
if big_button(ui, Icon::Close, "Close\nMaster View", true).clicked() {
|
||||
run(app, "view.closeMaster", json!({}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn handout_master(app: &mut SlideApp, ui: &mut Ui) {
|
||||
group(ui, |ui| {
|
||||
let r = big_button(ui, Icon::Layout, &format!("Slides Per\nPage: {}", app.ui.handout_per_page), true);
|
||||
|
||||
@@ -12,7 +12,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let in_master = app.session.active().is_some_and(|d| d.selection.target != Target::Slides);
|
||||
// Handout Master view has one page and no thumbnails, as in PowerPoint.
|
||||
if app.session.active().is_some_and(|d| d.selection.target == Target::Handout) {
|
||||
if app.session.active().is_some_and(|d| d.selection.target.is_page_master()) {
|
||||
return;
|
||||
}
|
||||
egui::Panel::left("thumbs")
|
||||
|
||||
+6
-4
@@ -161,10 +161,11 @@ Detail in [file-format-parity.md](file-format-parity.md).
|
||||
Detail in [animation-parity.md](animation-parity.md). 10–16 h; the pane is a **beta blocker**.
|
||||
|
||||
### Notes and Handout masters
|
||||
- **Evidence:** View ▸ Handout Master edits the handout master (header, date, footer and page
|
||||
number placeholders, background, slides-per-page preview) and PPTX keeps it. There's no Notes
|
||||
Master view yet, and PDF handouts don't use the handout master's text.
|
||||
- **Estimate:** 2–3 h (Notes Master view), 1 h (handout text in PDF).
|
||||
- **Evidence:** View ▸ Handout Master and View ▸ Notes Master edit both masters (placeholders,
|
||||
background, a slides-per-page preview for handouts) and PPTX keeps them. The notes master's slide
|
||||
image is an empty frame rather than a slide preview, and notes pages and PDF handouts don't take
|
||||
their layout and text from the masters yet.
|
||||
- **Estimate:** 2–3 h (notes pages and PDF handouts follow the masters).
|
||||
|
||||
### Edit points, text warp, 3-D
|
||||
- **Evidence:** checklist rows "Edit points" (P1 missing), "Text warp transforms" (partial: all 40
|
||||
@@ -258,6 +259,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-14 | minor | Notes Master view: the masters gap is now notes pages and PDF handouts following the masters |
|
||||
| 2026-10-14 | minor | Handout Master view: the masters gap is now the Notes Master view and handout text in PDF |
|
||||
| 2026-10-13 | minor | Owned PPTX corpus for tables, charts, animations, media, sections and masters. Handout-master text is kept; the corpus gap is now a larger set and PowerPoint reference renders |
|
||||
| 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) |
|
||||
|
||||
Reference in New Issue
Block a user