Compare commits

...
Author SHA1 Message Date
aiongg f21fb7259e Apply review fixes
- crates/engine/src/tests.rs
2026-11-05 00:42:25 +00:00
aiongg a21353c273 Merge main into fix/sec-engine-edit-shapes-dedupe 2026-11-05 00:33:39 +00:00
aiongg 52f87aca13 Merge pull request #1474 from zberry/pptx-show-media-controls
PPTX: Show media controls survives a save and reopen
2026-11-04 19:17:57 -05:00
aiongg dfee4c1e88 Merge pull request #1870 from Silvertree2010/fix/1864-next-and-previous-not-working-in
Make Previous/Next work in the web presenter popup
2026-11-04 19:03:38 -05:00
NetterSilverandClaude Opus 5.5 f321416b34 Make Previous/Next work in the web presenter popup
Fixes #1864

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:52:05 +01:00
aiongg 41f07a3c4e Merge main into pptx-show-media-controls 2026-11-04 23:51:52 +00:00
Dylan M. Taylor 50b9100971 engine: check the fill colour field in the repeated-ids test 2026-11-03 13:46:11 +00:00
Dylan M. Taylor 9405a06993 engine: edit each target shape once and read its box without copying it 2026-11-03 11:25:49 +00:00
Zach BerryandClaude Opus 5.5 b9eaaeb23b Test: hidden media controls are written as PowerPoint's p14 extension
The round trip now also checks the written presProps.xml: inside p:showPr, the extension uri and
p14:showMediaCtrls with its namespace declared on the element; the default writes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:03:02 -06:00
Zach BerryandClaude Sonnet 5.5 6303166a18 [PP1] show.setup showMediaControls survives a pptx save and reopen
PowerPoint stores Show media controls as the p14:showMediaCtrls extension inside
p:showPr (uri {2FDB2607-...}); absent means shown. Write it when hidden, read it back.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 15:12:26 -05:00
7 changed files with 67 additions and 8 deletions
+11 -7
View File
@@ -466,22 +466,26 @@ fn ctx_xfrm(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape:
/// Run `f` on each target shape (copy-on-write), materialising inherited boxes first.
pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut deckcraft_model::Shape) -> Result<()>) -> Result<Value> {
let ids = targets(s, p)?;
let mut ids = targets(s, p)?;
if ids.is_empty() {
return Err(bad(cmd, "no shapes selected"));
}
// A shape named twice is edited once.
let mut seen = std::collections::HashSet::new();
ids.retain(|id| seen.insert(*id));
s.edit(|doc, sel| {
let boxes: Vec<(ShapeId, Xfrm)> = {
let st_shapes: Vec<deckcraft_model::Shape> =
ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)).cloned()).collect();
st_shapes.iter().map(|sh| (sh.id, xfrm_of(doc, sel, sh))).collect()
};
// The boxes are read from the shapes in place: no copy of a shape (and its text) per id.
let boxes: std::collections::HashMap<ShapeId, Xfrm> = ids
.iter()
.filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)))
.map(|sh| (sh.id, xfrm_of(doc, sel, sh)))
.collect();
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
let mut n = 0;
for id in &ids {
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, *id) {
if sh.xfrm.is_none() {
sh.xfrm = boxes.iter().find(|(i, _)| i == id).map(|(_, x)| *x);
sh.xfrm = boxes.get(id).copied();
}
f(sh)?;
n += 1;
+12
View File
@@ -6483,6 +6483,18 @@ fn master_body_placeholder_format_keeps_per_level_formatting() {
assert_eq!(shown(&s), vec![Some(28.0), Some(24.0)]);
}
/// A shape named many times in one edit is edited once and not copied per mention.
#[test]
fn repeated_target_ids_edit_the_shape_once() {
let mut s = session();
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [100, 100, 300, 100], "text": "x".repeat(100_000)})).unwrap();
let id = r["id"].as_u64().unwrap();
let edited = s.execute("shape.fill", &json!({"ids": vec![id; 50_000], "color": "#ff0000"})).unwrap();
assert_eq!(edited, json!(1));
let v = s.execute("shape.inspect", &json!({"id": id})).unwrap();
assert_eq!(v["shape"]["fill"]["color"]["base"]["rgb"], json!({"a": 255, "b": 0, "g": 0, "r": 255}));
}
#[test]
fn slide_master_view_rejects_invalid_indices() {
let mut s = Session::with_new();
+1
View File
@@ -1351,6 +1351,7 @@ fn read_show(sp: &El, shows: &HashMap<String, String>) -> ShowSettings {
without_narration: sp.bool("showNarration") == Some(false),
without_animation: sp.bool("showAnimation") == Some(false),
use_timings: sp.bool("useTimings").unwrap_or(true),
show_media_controls: sp.find("showMediaCtrls").and_then(|e| e.bool("val")).unwrap_or(true),
..Default::default()
};
if sp.child("browse").is_some() {
+8
View File
@@ -1206,6 +1206,14 @@ fn pres_props_xml(p: &Presentation) -> Vec<u8> {
w.open0("p:penClr");
w.val("a:srgbClr", s.pen_color.hex());
w.close("p:penClr");
if !s.show_media_controls {
// "Show media controls" is PowerPoint's p14 extension; absent means shown.
w.open0("p:extLst");
w.open("p:ext", A::new().a("uri", "{2FDB2607-1784-4EEB-B798-7EB5836EED8A}"));
w.empty("p14:showMediaCtrls", A::new().a("xmlns:p14", NS_P14).a("val", 0));
w.close("p:ext");
w.close("p:extLst");
}
w.close("p:showPr");
w.close("p:presentationPr");
w.finish()
+28
View File
@@ -2258,6 +2258,34 @@ fn default_durations_match_powerpoint() {
}
}
/// Show media controls is PowerPoint's `p14:showMediaCtrls` extension inside `p:showPr`: off is
/// written there, under its extension uri and with the p14 namespace declared on the element;
/// on (the default) writes nothing.
#[test]
fn hidden_media_controls_survive_save_and_reopen() {
let props = |p: &Presentation| {
let bytes = deckcraft_pptx::export(p).expect("export");
let mut zip = zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("zip");
let mut x = String::new();
std::io::Read::read_to_string(&mut zip.by_name("ppt/presProps.xml").expect("presProps"), &mut x).expect("read");
x
};
let mut p = Presentation::default();
p.show.show_media_controls = false;
let xml = props(&p);
let show = xml.split("<p:showPr").nth(1).and_then(|r| r.split("</p:showPr>").next()).expect("p:showPr");
assert!(
show.contains(
"<p:ext uri=\"{2FDB2607-1784-4EEB-B798-7EB5836EED8A}\"><p14:showMediaCtrls xmlns:p14=\"http://schemas.microsoft.com/office/powerpoint/2010/main\" val=\"0\"/></p:ext>"
),
"{xml}"
);
assert!(!round(&p).show.show_media_controls);
let default = Presentation::default();
assert!(!props(&default).contains("showMediaCtrls"));
assert!(round(&default).show.show_media_controls);
}
#[test]
fn reflection_angles_and_skews_out_of_range_are_written_inside_the_schema() {
let mut p = Presentation::default();
+6
View File
@@ -1173,6 +1173,12 @@ fn remote_presenter_uses_the_audience_state_and_closes_with_the_show() {
h.run_steps(2);
assert_eq!(h.state().show.as_ref().unwrap().state.slide, slide);
}
// #1864: the web popup's Previous/Next buttons send the DOM key names.
for (name, slide) in [("ArrowRight", 1), ("ArrowLeft", 0)] {
bridge.borrow_mut().commands.push(Command::Key { name: name.into(), command: false, shift: false, alt: false, ctrl: false });
h.run_steps(2);
assert_eq!(h.state().show.as_ref().unwrap().state.slide, slide, "{name}");
}
bridge.borrow_mut().commands.push(Command::End);
h.run_steps(2);
assert!(h.state().show.is_none());
+1 -1
View File
@@ -94,7 +94,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
show.zoom_arm = false;
}
crate::presenter::Command::Key { name, command, shift, alt, ctrl } => {
if let Some(key) = egui::Key::ALL.iter().copied().find(|key| key.name() == name) {
if let Some(key) = egui::Key::from_name(&name) {
let modifiers = egui::Modifiers { command, shift, alt, ctrl, mac_cmd: command && !ctrl };
out.keep &= handle_keys(&mut show, &doc, now, vec![(key, modifiers)], false).0;
}