diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index 3ddaaf4c6..5e1879573 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -118,11 +118,21 @@ pub fn specs() -> Vec { let count = p.get("count").and_then(Value::as_u64).unwrap_or(1).clamp(1, 9999) as usize; let st = s.doc()?; let n = st.doc.page_count(); - let index = |k: &str| p.get(k).and_then(Value::as_u64).map(|v| usize::try_from(v).unwrap_or(usize::MAX)); + // A placement given (null included) must be a page index; only when neither is + // given do the pages go after the last. + if p.get("after").is_some() && p.get("before").is_some() { + return Err(bad("layout.pages.insert", "give `after` or `before`, not both")); + } + let index = |k: &str| match p.get(k) { + None => Ok(None), + Some(v) => v + .as_u64() + .map(|v| Some(usize::try_from(v).unwrap_or(usize::MAX))) + .ok_or_else(|| bad("layout.pages.insert", format!("`{k}` must be a page index, not {v}"))), + }; // The page the new ones follow; none: they go first. - let after = match (index("after"), index("before")) { - (Some(_), Some(_)) => return Err(bad("layout.pages.insert", "give `after` or `before`, not both")), - (None, Some(b)) => b.checked_sub(1), + let after = match (index("after")?, index("before")?) { + (_, Some(b)) => b.checked_sub(1), (a, None) => Some(a.unwrap_or(n.saturating_sub(1))), }; let parent = parent_ref("layout.pages.insert", &st.doc, p.get("parent"))?.unwrap_or_else(|| st.doc.parents.first().map(|x| x.id)); @@ -1884,10 +1894,22 @@ mod parent_margin_tests { assert_eq!(then[..3], now[..3]); assert_eq!(then[5], old[2]); assert_eq!((parent(&s, 3), parent(&s, 4)), (None, None)); + // Refused without a change or an undo step: both placements, even one of them null, and a + // placement that isn't a page index. let undo = s.doc().unwrap().history.undo.len(); - assert!(s.execute("layout.pages.insert", &json!({"after": 0, "before": 1})).is_err()); - assert_eq!(s.doc().unwrap().history.undo.len(), undo); - assert_eq!(ids(&s), then); + for bad in [ + json!({"after": 0, "before": 1}), + json!({"after": 0, "before": null}), + json!({"after": null, "before": 1}), + json!({"before": -1}), + json!({"before": "0"}), + json!({"before": null}), + json!({"after": 1.5}), + ] { + assert!(s.execute("layout.pages.insert", &bad).is_err(), "{bad}"); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{bad}"); + assert_eq!(ids(&s), then, "{bad}"); + } } #[test] diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 13ce41a6e..640888f24 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3132,7 +3132,8 @@ pub fn confirm(app: &mut DesignApp) -> Result { "ruby" => app.run("type.ruby", style_cjk::ruby_params(&d)), "insertPages" => { let n = app.session.active().map_or(1, |st| st.doc.page_count()); - let page = d.n("page").unwrap_or(0.0).max(0.0) as usize; + // The page the menu shows: a page past the end is the last. + let page = (d.n("page").unwrap_or(0.0).max(0.0) as usize).min(n.saturating_sub(1)); let mut p = json!({"count": d.n("count").unwrap_or(1.0).clamp(1.0, 9999.0) as u64}); match d.s("insert").as_str() { "before" => p["before"] = json!(page), @@ -7485,6 +7486,24 @@ mod tests { assert!(matches!(fill(&app, id), designcraft_color::Color::Rgb { .. }), "{:?}", fill(&app, id)); } + /// A page past the end shows as the last page in the dialog, and Before Page puts the new + /// page before that one rather than at the end. + #[test] + fn insert_pages_before_a_page_past_the_end_goes_before_the_last() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"pages": 4, "facingPages": false})).unwrap(); + let ids = |app: &DesignApp| { + let d = &app.session.doc().unwrap().doc; + (0..d.page_count()).map(|i| d.page(i).unwrap().id).collect::>() + }; + let old = ids(&app); + app.ui.dialog = Some(Dialog::new("insertPages", json!({"count": 1, "insert": "before", "page": 100, "parent": ""}))); + confirm(&mut app).unwrap(); + let now = ids(&app); + assert_eq!(now.len(), 5); + assert_eq!((now[..3].to_vec(), now[4]), (old[..3].to_vec(), old[3]), "the new page is fourth, before the old last page"); + } + /// The course's chapter 25: Insert Pages as InDesign's. It opens after the page in view with /// that page's parent; At Start of Document with B-Cover puts a B page first, and Before Page /// with [None] puts a page without a parent before the page chosen. diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index ddc27e2eb..ee60b7d27 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -1846,6 +1846,7 @@ const TABLE: &[(&str, [&str; 5])] = &[ ["Insert Math Expression…", "Insérer une expression mathématique…", "Insert Math Expression…", "Insert Math Expression…", "插入数学公式…"], ), ("Insert Pages…", ["Insert Pages…", "Insérer des pages…", "Insert Pages…", "Insert Pages…", "插入页面…"]), + ("Insert Pages", ["Seiten einfügen", "Insérer des pages", "Insertar páginas", "ページを挿入", "插入页面"]), ("Insert:", ["Einfügen:", "Insérer :", "Insertar:", "挿入:", "插入:"]), ("Parent:", ["Vorlagenseite:", "Gabarit :", "Página principal:", "親ページ:", "主页:"]), ("After Page", ["Nach Seite", "Après la page", "Después de la página", "ページの後", "页面后"]), diff --git a/crates/ui-egui/src/i18n/fr.rs b/crates/ui-egui/src/i18n/fr.rs index 78c5d5948..46558bb58 100644 --- a/crates/ui-egui/src/i18n/fr.rs +++ b/crates/ui-egui/src/i18n/fr.rs @@ -84,7 +84,6 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Insert Anchored Object", "Insérer un objet ancré"), ("Insert Cross-Reference", "Insérer une référence croisée"), ("Insert Math Expression", "Insérer une expression mathématique"), - ("Insert Pages", "Insérer des pages"), ("Internet Standard RGB (sRGB)", "RVB standard Internet (sRGB)"), ("Join the ArtCraft Discord", "Rejoindre le Discord d'ArtCraft"), ("Join the community", "Rejoindre la communauté"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index fb802b878..35561798a 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -33,7 +33,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Paste", "ჩასმა"), ("At End of Document", "დოკუმენტის ბოლოს"), ("At Start of Document", "დოკუმენტის დასაწყისში"), - ("Before Page", "გვერდამდე"), + ("Before Page", "გვერდის წინ"), ("After Page", "გვერდის შემდეგ"), ("Parent:", "საბაზო გვერდი:"), ("Insert:", "ჩასმა:"),