Merge PR #1058: Pages: deleting pages that contain objects asks first, with Don't show again

Conflicts:
- crates/ui-egui/src/dialogs.rs: kept the branch's dialog code (wider New Document, Crop Amount, Cell Options) and added #1058's deletePages title, width, body, confirm arm and delete_pages() at the branch's indentation.
- crates/ui-egui/src/i18n.rs, i18n/{ar,de,it,ja,ka,pt_br,uk}.rs: kept both sides.
This commit is contained in:
aiongg
2026-10-16 03:56:53 +00:00
15 changed files with 158 additions and 6 deletions
+1
View File
@@ -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.
+8
View File
@@ -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()));
+35 -1
View File
@@ -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<Value, String
app.run("table.setCell", Value::Object(params))
}
/// Delete absolute pages `pages` as the Pages panel and Layout › Pages › Delete Page do: when
/// they hold objects, InDesign's warning asks first, unless it was told not to.
pub fn delete_pages(app: &mut DesignApp, pages: Vec<usize>) -> Result<Value, String> {
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<Value, String> {
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.
+22
View File
@@ -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",
[
+4
View File
@@ -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 (,)", "تطبيق اللون (,)"),
+4
View File
@@ -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"),
+4
View File
@@ -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 (,)"),
+4
View File
@@ -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 (.)", "グラデーションを適用 (.)"),
+4
View File
@@ -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", "გასუფთავება"),
+4
View File
@@ -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"),
+4
View File
@@ -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 (,)", "Застосувати колір (,)"),
+3
View File
@@ -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<designcraft_color::cms::ColorSettings>,
/// 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(),
+1 -1
View File
@@ -1300,7 +1300,7 @@ fn run_ui_inner(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value
let Some(st) = app.session.active() else { return Some(Err("no document open".into())) };
let page = crate::canvas::current_page(app).unwrap_or(0);
let r = if id == "app.deletePage" {
app.run("layout.pages.delete", json!({"pages": [page]}))
crate::dialogs::delete_pages(app, vec![page])
} else {
let spread = st.doc.page_loc(page).map_or(0, |l| l.0);
app.run("layout.pages.duplicateSpread", json!({"spread": spread}))
+59 -3
View File
@@ -268,7 +268,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if icons::button(ui, "trash", 20.0, false, crate::i18n::tr(&app.ui.language, "Delete selected pages")).clicked() {
let _ = app.run("layout.pages.delete", json!({"pages": [cur]}));
let _ = crate::dialogs::delete_pages(app, vec![cur]);
}
// Create new page; a page dropped on it is duplicated at the end of the document.
let plus = icons::button(ui, "plus", 20.0, dragging, crate::i18n::tr(&app.ui.language, "Create new page"));
@@ -358,12 +358,12 @@ fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Docu
ui.close();
}
if item(ui, "Delete Page") {
let _ = app.run("layout.pages.delete", json!({"pages": [abs]}));
let _ = crate::dialogs::delete_pages(app, vec![abs]);
ui.close();
}
if pages_in_spread > 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::<Vec<_>>()}));
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());
+1 -1
View File
@@ -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() {