Integration: #869 Alt-drag copies a page in one command and one undo step (layout.pages.duplicateSpread takes to)

(cherry picked from commit 7889b25a379fdf27dd128bc49512bc3c0cbfcbfd)
This commit is contained in:
aiongg authored and moppeeer committed 2026-10-14 09:43:06 +02:00
1 parent 15fccae6b8
commit f4efb7cc5c
3 files changed
+61 -14

No files matched your search

+21 -2
View File
@@ -140,13 +140,18 @@ pub fn specs() -> Vec<CommandSpec> {
"Duplicate Spread",
["Layout", "Pages"],
None,
"{spread, atEnd?: bool (Duplicate Spread at End of Document; the Pages panel's New Page button takes a dropped page there)} → {page: the copy's first page (0-based)} — the spread's pages (size, margins, columns, guides, liquid rule) and items",
"{spread, atEnd?: bool (Duplicate Spread at End of Document; the Pages panel's New Page button takes a dropped page there), to?: page index the copied pages move to, in the same undo step (the Pages panel's Alt-drag)} → {page: the copy's first page (0-based)} — the spread's pages (size, margins, columns, guides, liquid rule) and items",
has_doc,
|s, p| {
let si = p.get("spread").and_then(Value::as_u64).unwrap_or(0) as usize;
let at_end = p.get("atEnd").and_then(Value::as_bool).unwrap_or(false);
let to = p.get("to").and_then(Value::as_u64).map(|t| t as usize);
s.edit(|d, _| {
let sp = d.spreads.get(si).cloned().ok_or_else(|| bad("layout.pages.duplicateSpread", "no such spread"))?;
// The copies land at `to`..`to + n` of the document with them in it.
if to.is_some_and(|t| t > d.page_count()) {
return Err(bad("layout.pages.duplicateSpread", "no such page"));
}
let after = if at_end { d.page_count() - 1 } else { d.first_page_of_spread(si) + sp.pages.len() - 1 };
let parent = sp.pages.first().and_then(|p| p.parent);
d.insert_pages(Some(after), sp.pages.len(), parent)?;
@@ -169,7 +174,21 @@ pub fn specs() -> Vec<CommandSpec> {
let ids: Vec<_> = sp.items.iter().map(|i| i.id).collect();
let (nsi, _) = d.page_loc(after + 1).ok_or_else(|| bad("layout.pages.duplicateSpread", "insert failed"))?;
super::object::duplicate_from(d, &src, &ids, designcraft_doc::SpreadRef::Doc(nsi), designcraft_geom::Vec2::ZERO)?;
Ok(json!({"page": after + 1}))
let first = after + 1;
let Some(to) = to else { return Ok(json!({"page": first})) };
// Moving a page shifts the ones between; order the moves so each copy keeps
// its place among the others.
let n = sp.pages.len();
if first < to {
for k in (0..n).rev() {
d.move_page(first + k, to + k)?;
}
} else {
for k in 0..n {
d.move_page(first + k, to + k)?;
}
}
Ok(json!({"page": to}))
})
}
),
+28
View File
@@ -695,3 +695,31 @@ fn type_tool_frames_snap_to_the_margins() {
assert!((r.x0 - margin.x0).abs() < 1e-6 && (r.y0 - margin.y0).abs() < 1e-6, "press point {r:?} vs {margin:?}");
assert!((r.x1 - margin.x1).abs() < 1e-6, "right edge {} vs {}", r.x1, margin.x1);
}
#[test]
fn duplicating_a_page_to_an_index_is_one_undo_step() {
let mut s = Session::new();
s.execute("file.new", &json!({"facingPages": false})).unwrap();
s.execute("layout.pages.insert", &json!({"count": 2})).unwrap();
for (page, width) in [(1, 300), (2, 400), (3, 500)] {
s.execute("layout.pageSize", &json!({"pages": [page], "width": width, "height": 600})).unwrap();
}
let widths = |s: &Session| {
let d = &s.doc().unwrap().doc;
(0..d.page_count()).map(|i| d.page_loc(i).map(|(si, pi)| d.spreads[si].pages[pi].width)).collect::<Option<Vec<_>>>().unwrap()
};
// Backwards: the last page copied to the front.
let r = s.execute("layout.pages.duplicateSpread", &json!({"spread": 2, "to": 0})).unwrap();
assert_eq!(r["page"], 0);
assert_eq!(widths(&s), [500.0, 300.0, 400.0, 500.0]);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(widths(&s), [300.0, 400.0, 500.0]);
// Forwards: the first page copied to the end.
s.execute("layout.pages.duplicateSpread", &json!({"spread": 0, "to": 3})).unwrap();
assert_eq!(widths(&s), [300.0, 400.0, 500.0, 300.0]);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(widths(&s), [300.0, 400.0, 500.0]);
// Past the end: refused, nothing changes.
assert!(s.execute("layout.pages.duplicateSpread", &json!({"spread": 0, "to": 4})).is_err());
assert_eq!(widths(&s), [300.0, 400.0, 500.0]);
}
+12 -12
View File
@@ -239,21 +239,21 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
});
});
});
// Alt-drag (InDesign's Option-drag) copies the page to where it's dropped.
let copy = ui.input(|i| i.modifiers.alt);
// Alt-drag copies a page that is a spread of its own to where it's dropped, in one undo
// step; a page from a larger spread moves as usual.
let alt = ui.input(|i| i.modifiers.alt);
let copy = |from: usize| {
let (si, _) = doc.page_loc(from)?;
(alt && doc.spreads.get(si).is_some_and(|s| s.pages.len() == 1)).then_some(si)
};
match drop {
Some((PageDrag::Page(from), to)) if copy => {
if let Some((si, _)) = doc.page_loc(from)
&& let Ok(r) = app.run("layout.pages.duplicateSpread", json!({"spread": si}))
&& let Some(new) = r["page"].as_u64().map(|p| p as usize)
&& new != to
{
let _ = app.run("layout.pages.move", json!({"from": new, "to": to}));
Some((PageDrag::Page(from), to)) => {
if let Some(si) = copy(from) {
let _ = app.run("layout.pages.duplicateSpread", json!({"spread": si, "to": to}));
} else if from != to {
let _ = app.run("layout.pages.move", json!({"from": from, "to": to}));
}
}
Some((PageDrag::Page(from), to)) if from != to => {
let _ = app.run("layout.pages.move", json!({"from": from, "to": to}));
}
Some((PageDrag::Parent(prefix), to)) => {
let _ = app.run("layout.pages.applyParent", json!({"pages": [to], "parent": prefix}));
}