diff --git a/ROADMAP.md b/ROADMAP.md index 31b0c9427..1bd4df999 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -441,6 +441,7 @@ Newest first. - 2026-10-15: Measures in inches show four decimals, as InDesign's fields do (A4 is 8.2677 × 11.6929 in, a 1p0 gutter 0.1667 in); other units keep three. - 2026-10-15: Ruler menus as InDesign's: right-clicking where the rulers meet sets the units of both; each ruler's menu lists InDesign's units and Hide Rulers, the horizontal one Delete All Guides on Spread. - 2026-10-15: Properties › Edit Page works as InDesign's: the Page tool on the page in view, and Properties shows that page alone (Page Size: page, preset, orientation, W/H; Margins and Columns; Quick Actions) with Back. It opened Document Setup, which changes every page. +- 2026-10-15: Deleting pages that contain objects asks first, as InDesign does ("The affected page contains objects. Delete the page anyway?", with Don't show again), from the Pages panel, Layout › Pages › Delete Page and Properties. - **2026-10-14** · The Type tool draws a new text frame over a locked empty frame, as InDesign does, instead of turning the locked frame into a text frame. No readiness percentage change. - **2026-10-14** · 100% black shows as pure black on screen by default, as InDesign's Appearance of Black does out of the box (Display All Blacks as Rich Black); Preferences › Appearance of Black › Display All Blacks Accurately still shows it as the dark grey it prints, and a saved choice is kept. No readiness percentage change. - **2026-10-14** · Pathfinder keeps the frontmost object, as InDesign does: a photo placed over shapes and intersected with them shows through the shapes (it was dropped, and the shapes kept their black fill); Add, Exclude Overlap and Minus Back take the front object's fill too, Subtract still keeps the back one. No readiness percentage change. diff --git a/crates/doc/src/build.rs b/crates/doc/src/build.rs index f4482c4fe..20643ef4b 100644 --- a/crates/doc/src/build.rs +++ b/crates/doc/src/build.rs @@ -463,6 +463,14 @@ impl Document { } /// Delete absolute pages (items whose centre is on a deleted page are deleted too). + /// Whether absolute page `abs` holds page items: the ones Delete Pages removes with it + /// (an item belongs to the page its centre is over). + pub fn page_has_items(&self, abs: usize) -> bool { + let Some((si, pi)) = self.page_loc(abs) else { return false }; + let Some(sp) = self.spreads.get(si) else { return false }; + sp.items.iter().any(|it| sp.page_at_x(it.bounds().center().x) == Some(pi)) + } + pub fn delete_pages(&mut self, pages: &[usize]) -> Result<()> { if pages.len() >= self.page_count() { return Err(DocError::Invalid("a document needs at least one page".into())); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 935d88d26..aa2a903cb 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -2004,6 +2004,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { "userDictionary" => crate::i18n::tr(&app.ui.language, "User Dictionary"), "newWorkspace" => crate::i18n::tr(&app.ui.language, "New Workspace"), "closeDocument" => crate::i18n::tr(&app.ui.language, "Unsaved Changes"), + "deletePages" => crate::i18n::tr(&app.ui.language, "Warning"), "updateLink" => crate::i18n::tr(&app.ui.language, "Update Link"), "menus" => crate::i18n::tr(&app.ui.language, "Menu Customization"), "importOptions" => crate::i18n::tr(&app.ui.language, "Import Options"), @@ -2035,7 +2036,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { let available = (ctx.content_rect().size() - frame_margin - egui::vec2(16.0, 16.0)).max(egui::Vec2::splat(1.0)); let preferred_width: f32 = match d.id.as_str() { "alert" => 440.0, - "closeDocument" | "updateLink" => 380.0, + "closeDocument" | "updateLink" | "deletePages" => 380.0, "newDocument" => 960.0, "numberingSection" => 560.0, crate::effects_dialog::ID => 720.0, @@ -2504,6 +2505,22 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { .replace("{}", &d.s("title")); crate::rtl::label(ui, text); } + "deletePages" => { + let several = d.fields.get("pages").and_then(Value::as_array).is_some_and(|p| p.len() > 1); + crate::rtl::label( + ui, + crate::i18n::tr( + &app.ui.language, + if several { + "The affected pages contain objects. Delete the pages anyway?" + } else { + "The affected page contains objects. Delete the page anyway?" + }, + ), + ); + ui.add_space(8.0); + check(ui, &mut d, "dontShowAgain", crate::i18n::tr(&app.ui.language, "Don't show again")); + } "updateLink" => { let text = crate::i18n::tr( &app.ui.language, @@ -2859,6 +2876,17 @@ fn confirm_cell_options(app: &mut DesignApp, d: &Dialog) -> Result) -> Result { + let has_items = app.session.active().is_some_and(|st| pages.iter().any(|&abs| st.doc.page_has_items(abs))); + if has_items && !app.ui.skip_delete_pages_warning { + app.ui.dialog = Some(Dialog::new("deletePages", json!({"pages": pages}))); + return Ok(Value::Null); + } + app.run("layout.pages.delete", json!({"pages": pages})) +} + /// Close the open dialog without applying it (Cancel, Esc, the control channel): a previewing /// dialog puts the document back as it was, and Preferences puts back the interface language it /// opened with, since choosing one previews it at once. @@ -3292,6 +3320,12 @@ pub fn confirm(app: &mut DesignApp) -> Result { app.run("document.preferences", doc) } "newWorkspace" => app.run("window.newWorkspace", json!({"name": d.s("name")})), + "deletePages" => { + if d.b("dontShowAgain") { + app.ui.skip_delete_pages_warning = true; + } + app.run("layout.pages.delete", json!({"pages": d.fields.get("pages").cloned().unwrap_or_else(|| json!([]))})) + } "updateLink" => crate::panels::properties::run_update_links(app, d.fields.get("target").cloned().unwrap_or_else(|| json!({}))), "closeDocument" => { // The document may have moved in the tab row (or gone) while the dialog was open. diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index e8558a4fc..9cf42b8d1 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -97,6 +97,28 @@ const TABLE: &[(&str, [&str; 5])] = &[ ), ("Back", ["Zurück", "Retour", "Atrás", "戻る", "返回"]), ("Margins and Columns", ["Ränder und Spalten", "Marges et colonnes", "Márgenes y columnas", "マージン・段組", "边距和分栏"]), + ("Warning", ["Warnung", "Avertissement", "Advertencia", "警告", "警告"]), + ("Don't show again", ["Nicht mehr anzeigen", "Ne plus afficher", "No volver a mostrar", "今後表示しない", "不再显示"]), + ( + "The affected page contains objects. Delete the page anyway?", + [ + "Die betroffene Seite enthält Objekte. Seite trotzdem löschen?", + "La page concernée contient des objets. Supprimer quand même la page ?", + "La página afectada contiene objetos. ¿Desea eliminarla de todos modos?", + "対象のページにはオブジェクトが含まれています。ページを削除しますか?", + "受影响的页面包含对象。仍要删除该页面吗?", + ], + ), + ( + "The affected pages contain objects. Delete the pages anyway?", + [ + "Die betroffenen Seiten enthalten Objekte. Seiten trotzdem löschen?", + "Les pages concernées contiennent des objets. Supprimer quand même les pages ?", + "Las páginas afectadas contienen objetos. ¿Desea eliminarlas de todos modos?", + "対象のページにはオブジェクトが含まれています。ページを削除しますか?", + "受影响的页面包含对象。仍要删除这些页面吗?", + ], + ), ( "Bulleted & Numbered Lists", [ diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 5f20d36c0..9164b5976 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -48,6 +48,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Diffused", "منتشرة"), ("Inches Decimal", "بوصات عشرية"), ("Back", "رجوع"), + ("Warning", "تحذير"), + ("Don't show again", "عدم الإظهار مرة أخرى"), + ("The affected page contains objects. Delete the page anyway?", "تحتوي الصفحة المعنية على كائنات. هل تريد حذف الصفحة على أي حال؟"), + ("The affected pages contain objects. Delete the pages anyway?", "تحتوي الصفحات المعنية على كائنات. هل تريد حذف الصفحات على أي حال؟"), ("Bulleted & Numbered Lists", "القوائم ذات التعداد النقطي والرقمي"), ("Current Page Number", "رقم الصفحة الحالية"), ("Apply Color (,)", "تطبيق اللون (,)"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 5ba4c9f3e..cb69f2e7e 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -33,6 +33,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Paste", "Einfügen"), ("Inches Decimal", "Zoll dezimal"), ("Back", "Zurück"), + ("Warning", "Warnung"), + ("Don't show again", "Nicht mehr anzeigen"), + ("The affected page contains objects. Delete the page anyway?", "Die betroffene Seite enthält Objekte. Seite trotzdem löschen?"), + ("The affected pages contain objects. Delete the pages anyway?", "Die betroffenen Seiten enthalten Objekte. Seiten trotzdem löschen?"), ("Clear", "Löschen"), ("Duplicate", "Duplizieren"), ("Select All", "Alles auswählen"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 13f1ab2b1..c7876de4b 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -47,6 +47,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Inches Decimal", "Pollici decimali"), ("Back", "Indietro"), ("Margins and Columns", "Margini e colonne"), + ("Warning", "Avviso"), + ("Don't show again", "Non mostrare più"), + ("The affected page contains objects. Delete the page anyway?", "La pagina interessata contiene oggetti. Eliminarla comunque?"), + ("The affected pages contain objects. Delete the pages anyway?", "Le pagine interessate contengono oggetti. Eliminarle comunque?"), ("Bulleted & Numbered Lists", "Elenchi puntati e numerati"), ("Current Page Number", "Numero di pagina corrente"), ("Apply Color (,)", "Applica colore (,)"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 7073ce3c4..4d12cfa3a 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -50,6 +50,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Diffused", "拡散"), ("Inches Decimal", "インチ(10 進)"), ("Back", "戻る"), + ("Warning", "警告"), + ("Don't show again", "今後表示しない"), + ("The affected page contains objects. Delete the page anyway?", "対象のページにはオブジェクトが含まれています。ページを削除しますか?"), + ("The affected pages contain objects. Delete the pages anyway?", "対象のページにはオブジェクトが含まれています。ページを削除しますか?"), ("Bulleted & Numbered Lists", "箇条書き"), ("Apply Color (,)", "カラーを適用 (,)"), ("Apply Gradient (.)", "グラデーションを適用 (.)"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 0add5446f..d80536170 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -33,6 +33,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Paste", "ჩასმა"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), + ("Warning", "გაფრთხილება"), + ("Don't show again", "აღარ აჩვენო"), + ("The affected page contains objects. Delete the page anyway?", "გვერდი შეიცავს ობიექტებს. მაინც წავშალო გვერდი?"), + ("The affected pages contain objects. Delete the pages anyway?", "გვერდები შეიცავს ობიექტებს. მაინც წავშალო გვერდები?"), ("Reverse Path", "ტრაექტორიის შებრუნება"), ("Set Anchor", "საყრდენი წერტილის დაყენება"), ("Clear", "გასუფთავება"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 04e6a024d..ff2a9f560 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -50,6 +50,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Inches Decimal", "Polegadas decimais"), ("Back", "Voltar"), ("Margins and Columns", "Margens e colunas"), + ("Warning", "Aviso"), + ("Don't show again", "Não mostrar novamente"), + ("The affected page contains objects. Delete the page anyway?", "A página afetada contém objetos. Excluir a página mesmo assim?"), + ("The affected pages contain objects. Delete the pages anyway?", "As páginas afetadas contêm objetos. Excluir as páginas mesmo assim?"), ("Bulleted & Numbered Lists", "Listas com marcadores e numeradas"), ("Show Hidden Characters", "Mostrar caracteres ocultos"), ("Current Page Number", "Número da página atual"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 83064a108..aa36f75d6 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -48,6 +48,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Inches Decimal", "Дюйми (десяткові)"), ("Back", "Назад"), ("Margins and Columns", "Поля та колонки"), + ("Warning", "Попередження"), + ("Don't show again", "Більше не показувати"), + ("The affected page contains objects. Delete the page anyway?", "Сторінка містить об’єкти. Усе одно видалити сторінку?"), + ("The affected pages contain objects. Delete the pages anyway?", "Сторінки містять об’єкти. Усе одно видалити сторінки?"), ("Bulleted & Numbered Lists", "Марковані й нумеровані списки"), ("Current Page Number", "Номер поточної сторінки"), ("Apply Color (,)", "Застосувати колір (,)"), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 11ccf6085..49dbde30a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -203,6 +203,8 @@ pub struct UiState { pub flattener_preview: bool, /// Tab / Shift+Tab: 1 = every panel hidden, 2 = all but the Tools panel (0 = shown). pub hidden_panels: u8, + /// "Don't show again" in the warning before deleting pages that contain objects. + pub skip_delete_pages_warning: bool, /// Edit › Color Settings (re-applied at start). pub color_settings: Option, /// View › Proof Colors and View › Proof Setup. @@ -359,6 +361,7 @@ impl Default for UiState { tag_markers: false, flattener_preview: false, hidden_panels: 0, + skip_delete_pages_warning: false, color_settings: None, proof_colors: false, proof_setup: Default::default(), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index df44bfd5b..b799d3a7c 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1300,7 +1300,7 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option 1 && item(ui, "Delete Spread") { let first = doc.first_page_of_spread(si); - let _ = app.run("layout.pages.delete", json!({"pages": (first..first.saturating_add(pages_in_spread)).collect::>()})); + let _ = crate::dialogs::delete_pages(app, (first..first.saturating_add(pages_in_spread)).collect()); ui.close(); } ui.separator(); @@ -544,6 +544,62 @@ mod tests { assert_eq!(h.state().session.doc().unwrap().doc.page_count(), 4, "only the clicked page"); } + /// As in InDesign: deleting a page with objects asks first; an empty page goes at once, and + /// "Don't show again" stops the question. + #[test] + fn deleting_a_page_with_objects_asks_first() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"pages": 5, "facingPages": true})).unwrap(); + let doc = app.session.doc().unwrap().doc.clone(); + let (si, pi) = doc.page_loc(2).unwrap(); + let x = doc.spreads[si].pages[pi].x; + app.run("frame.create", json!({"spread": si, "rect": [x + 36.0, 36.0, x + 200.0, 200.0]})).unwrap(); + let count = |h: &Harness<'_, DesignApp>| h.state().session.doc().unwrap().doc.page_count(); + let mut h = menu_harness(app, 2, si); + h.get_by_label("Delete Page").click(); + h.run_steps(3); + assert_eq!(count(&h), 5, "nothing deleted yet"); + let d = h.state().ui.dialog.clone().unwrap(); + assert_eq!((d.id.as_str(), d.fields["pages"].clone()), ("deletePages", json!([2]))); + // Cancel keeps the page. + crate::dialogs::cancel(h.state_mut()); + assert_eq!(count(&h), 5); + // The whole spread: one question for both pages. + h.get_by_label("Delete Spread").click(); + h.run_steps(3); + assert_eq!(h.state().ui.dialog.as_ref().unwrap().fields["pages"], json!([1, 2])); + crate::dialogs::cancel(h.state_mut()); + // OK deletes it. + h.get_by_label("Delete Page").click(); + h.run_steps(3); + crate::dialogs::confirm(h.state_mut()).unwrap(); + assert_eq!(count(&h), 4); + assert!(!h.state().ui.skip_delete_pages_warning); + // An empty page goes without a question. + let app = h.state_mut(); + crate::dialogs::delete_pages(app, vec![0]).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(app.session.doc().unwrap().doc.page_count(), 3); + // "Don't show again". + let doc = app.session.doc().unwrap().doc.clone(); + let (si, pi) = doc.page_loc(1).unwrap(); + let x = doc.spreads[si].pages[pi].x; + app.run("frame.create", json!({"spread": si, "rect": [x + 36.0, 36.0, x + 200.0, 200.0]})).unwrap(); + app.run("frame.create", json!({"spread": si, "rect": [x + 36.0, 300.0, x + 200.0, 400.0]})).unwrap(); + crate::dialogs::delete_pages(app, vec![1]).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("dontShowAgain".into(), json!(true)); + crate::dialogs::confirm(app).unwrap(); + assert!(app.ui.skip_delete_pages_warning); + assert_eq!(app.session.doc().unwrap().doc.page_count(), 2); + let doc = app.session.doc().unwrap().doc.clone(); + let (si, pi) = doc.page_loc(0).unwrap(); + let x = doc.spreads[si].pages[pi].x; + app.run("frame.create", json!({"spread": si, "rect": [x + 36.0, 36.0, x + 200.0, 200.0]})).unwrap(); + crate::dialogs::delete_pages(app, vec![0]).unwrap(); + assert!(app.ui.dialog.is_none(), "no more questions"); + assert_eq!(app.session.doc().unwrap().doc.page_count(), 1); + } + #[test] fn the_panel_menu_follows_indesigns_order() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index de8edac08..86ed7faa0 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -458,7 +458,7 @@ fn document_sections(app: &mut DesignApp, ui: &mut Ui) { let _ = app.run("layout.pages.insert", json!({"after": cur, "count": 1})); } if del { - let _ = app.run("layout.pages.delete", json!({"pages": [cur]})); + let _ = crate::dialogs::delete_pages(app, vec![cur]); } ui.add_space(3.0); if outline_button(ui, crate::i18n::tr(&app.ui.language, "Edit Page"), fw).clicked() {