mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 03:06:56 -06:00
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:
@@ -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.
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
[
|
||||
|
||||
@@ -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 (,)", "تطبيق اللون (,)"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 (,)"),
|
||||
|
||||
@@ -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 (.)", "グラデーションを適用 (.)"),
|
||||
|
||||
@@ -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", "გასუფთავება"),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 (,)", "Застосувати колір (,)"),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user