feat(ui): Pages panel menu in InDesign's order

The Pages panel menu in Chris Cicchino's "Adobe InDesign – Page Tool
and Liquid Layout" (0:47) reads: Insert Pages…, Move Pages…, Duplicate
Spread, Duplicate Spread at End of Document, Delete Spread, … New
Parent…, Apply Parent to Pages…, Override All Parent Page Items, Parent
Pages ▸, Create Alternate Layout…, Numbering & Section Options…, Allow
Selected Spread to Shuffle. DesignCraft's menu (and the page thumbnails'
context menu) had Insert Page After, Delete Page, Duplicate Spread, Add
to Previous Spread, Allow Spread to Shuffle and the parent items in
another order.

- Pages panel menu and page context menu in InDesign's order and labels:
  Insert Pages… and Move Pages… open their dialogs for the page; Delete
  Spread for a spread of several pages; Parent Pages ▸ holds Remove All
  Local Overrides and Detach All Objects from Parent; Create Alternate
  Layout… and Numbering & Section Options… open theirs. Add to Previous
  Spread stays last (InDesign drags pages into spreads instead).
- Translations (pt-BR gains the existing items it lacked).

Builds on #869 (Duplicate Spread at End of Document).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
moppeeerandClaude Opus 5.5 committed 2026-10-14 02:15:27 +02:00
1 parent 3939b825d3
commit 8156e575bc
9 files changed
+142 -29

No files matched your search

+1
View File
@@ -129,6 +129,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
Newest first.
- **2026-10-14** · The Pages panel menu follows InDesign's order: Insert Pages…, Move Pages…, Duplicate Spread (and at End of Document), Delete Page/Spread, New Parent…, Apply Parent to Pages, Override All Parent Page Items, Parent Pages, Create Alternate Layout…, Numbering & Section Options…, Allow Selected Spread to Shuffle. No readiness percentage change.
- **2026-10-14** · Pages panel: Alt-drag copies a page, a page dropped on Create new page is duplicated at the end, Edit page size offers the presets and Custom Page Size…; duplicated pages keep their own size, rule and guides; thumbnails of pages of their own size stay in their rows. No readiness percentage change.
- **2026-10-12** · macOS Dock Quit uses the same unsaved-document prompts as window and menu Quit. Repeated requests keep the current answers; Cancel retains every tab and unsaved recovery checkpoint, and completed Save/Don't Save answers exit through the ordinary event loop. No readiness percentage change.
- **2026-10-12** · Orderly native exit writes the latest recovery state after finalizing current quit approvals; incomplete or stale plans keep their documents, and Undo to the saved state clears stale recovery. Unavailable or failed recovery is reported. Cancelable macOS Dock Quit remains open (#544). No readiness percentage change.
+22
View File
@@ -296,6 +296,28 @@ const TABLE: &[(&str, [&str; 5])] = &[
("Document Setup", ["Document Setup", "Document Setup", "Document Setup", "Document Setup", "文档设置"]),
("Drop Shadow", ["Drop Shadow", "Drop Shadow", "Drop Shadow", "Drop Shadow", "投影"]),
("Duplicate Spread", ["Duplicate Spread", "Duplicate Spread", "Duplicate Spread", "Duplicate Spread", "复制跨页"]),
(
"Duplicate Spread at End of Document",
[
"Druckbogen am Dokumentende duplizieren",
"Dupliquer la planche à la fin du document",
"Duplicar pliego al final del documento",
"ドキュメントの最後にスプレッドを複製",
"在文档末尾复制跨页",
],
),
("Delete Spread", ["Druckbogen löschen", "Supprimer la planche", "Eliminar pliego", "スプレッドを削除", "删除跨页"]),
("Parent Pages", ["Übergeordnete Seiten", "Pages parents", "Páginas principales", "親ページ", "父页面"]),
(
"Apply Parent to Pages",
[
"Übergeordnete Seite auf Seiten anwenden",
"Appliquer le parent aux pages",
"Aplicar principal a páginas",
"親ページを適用",
"将父页面应用于页面",
],
),
("Email", ["Email", "Email", "Email", "Email", "电子邮件"]),
("Embed", ["Embed", "Embed", "Embed", "Embed", "嵌入"]),
("Export PDF", ["PDF exportieren", "Exporter en PDF", "Exportar PDF", "PDF を書き出し", "导出PDF"]),
+4
View File
@@ -864,6 +864,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Document Grid", "شبكة المستند"),
("Document Recovery Data", "بيانات استرداد المستند"),
("Duplicate Spread", "تكرار الصفحات المتقابلة"),
("Duplicate Spread at End of Document", "تكرار الانتشار في نهاية المستند"),
("Delete Spread", "حذف الانتشار"),
("Parent Pages", "الصفحات الأصل"),
("Apply Parent to Pages", "تطبيق الأصل على الصفحات"),
("Email", "بريد إلكتروني"),
("Embed", "تضمين"),
("Fast", "سريع"),
+4
View File
@@ -177,6 +177,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Document Setup", "Impostazione documento"),
("Drop Shadow", "Ombra esterna"),
("Duplicate Spread", "Duplica doppia pagina"),
("Duplicate Spread at End of Document", "Duplica pagine affiancate alla fine del documento"),
("Delete Spread", "Elimina pagine affiancate"),
("Parent Pages", "Pagine principali"),
("Apply Parent to Pages", "Applica pagina principale alle pagine"),
("Email", "E-mail"),
("Embed", "Incorpora"),
("Export XML…", "Esporta XML…"),
+3
View File
@@ -179,6 +179,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Document Setup", "ドキュメント設定"),
("Drop Shadow", "ドロップシャドウ"),
("Duplicate Spread", "スプレッドを複製"),
("Duplicate Spread at End of Document", "ドキュメントの最後にスプレッドを複製"),
("Delete Spread", "スプレッドを削除"),
("Parent Pages", "親ページ"),
("Email", "メール"),
("Embed", "埋め込み"),
("Export XML…", "XML を書き出し…"),
+12
View File
@@ -866,6 +866,18 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Document Grid", "Grade do documento"),
("Document Recovery Data", "Dados de recuperação do documento"),
("Duplicate Spread", "Duplicar página dupla"),
("Insert Pages…", "Inserir páginas…"),
("Move Pages…", "Mover páginas…"),
("New Parent…", "Nova página principal…"),
("Override All Parent Page Items", "Substituir todos os itens da página principal"),
("Remove All Local Overrides", "Remover todas as substituições locais"),
("Detach All Objects from Parent", "Desanexar todos os objetos da página principal"),
("Create Alternate Layout…", "Criar layout alternativo…"),
("Allow Selected Spread to Shuffle", "Permitir que a página espelhada selecionada seja reorganizada"),
("Duplicate Spread at End of Document", "Duplicar página espelhada no final do documento"),
("Delete Spread", "Excluir página espelhada"),
("Parent Pages", "Páginas principais"),
("Apply Parent to Pages", "Aplicar página principal às páginas"),
("Email", "E-mail"),
("Embed", "Embutir"),
("Fast", "Rápido"),
+4
View File
@@ -180,6 +180,10 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Document Setup", "Параметри документа"),
("Drop Shadow", "Тінь"),
("Duplicate Spread", "Дублювати розворот"),
("Duplicate Spread at End of Document", "Дублювати розворот у кінці документа"),
("Delete Spread", "Видалити розворот"),
("Parent Pages", "Батьківські сторінки"),
("Apply Parent to Pages", "Застосувати батьківську до сторінок"),
("Email", "Електронна пошта"),
("Embed", "Вбудувати"),
("Export XML…", "Експортувати XML…"),
+1 -1
View File
@@ -617,7 +617,7 @@ fn legacy_dock_menu_button_opens_the_active_panel_menu() {
}
h.run_steps(2);
assert!(egui::Popup::is_any_open(&h.ctx), "the three-line button must open a menu");
h.get_by_label("Insert Page After").click();
h.get_by_label("Duplicate Spread").click();
h.run_steps(3);
assert_eq!(h.state().session.active().unwrap().doc.page_count(), pages + 1);
assert!(!egui::Popup::is_any_open(&h.ctx));
+91 -28
View File
@@ -271,47 +271,39 @@ pub fn panel_menu(app: &mut DesignApp, ui: &mut egui::Ui) {
}
fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Document, abs: usize, si: usize) {
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Insert Page After"))).clicked() {
let _ = app.run("layout.pages.insert", json!({"after": abs, "count": 1}));
let lang = app.ui.language.clone();
let tr = |s: &str| crate::i18n::tr(&lang, s).to_owned();
let pages_in_spread = doc.spreads.get(si).map_or(1, |sp| sp.pages.len());
let item = |ui: &mut egui::Ui, label: &str| ui.button(crate::rtl::widget(ui, tr(label))).clicked();
// InDesign's order: pages, parents, layouts and numbering, shuffling.
if item(ui, "Insert Pages…") {
app.ui.dialog = Some(crate::dialogs::Dialog::new("cmd:layout.pages.insert", json!({"after": abs, "count": 1})));
ui.close();
}
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Page"))).clicked() {
let _ = app.run("layout.pages.delete", json!({"pages": [abs]}));
if item(ui, "Move Pages…") {
app.ui.dialog = Some(crate::dialogs::Dialog::new("cmd:layout.pages.move", json!({"from": abs, "to": abs})));
ui.close();
}
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Duplicate Spread"))).clicked() {
if item(ui, "Duplicate Spread") {
let _ = app.run("layout.pages.duplicateSpread", json!({"spread": si}));
ui.close();
}
if si > 0
&& ui
.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Add to Previous Spread")))
.on_hover_ui(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Spreads of up to 10 pages"));
})
.clicked()
{
let _ = app.run("layout.pages.toSpread", json!({"page": abs + 1, "spread": si - 1}));
if item(ui, "Duplicate Spread at End of Document") {
let _ = app.run("layout.pages.duplicateSpread", json!({"spread": si, "atEnd": true}));
ui.close();
}
let mut shuffle = doc.spreads.get(si).is_none_or(|sp| sp.allow_shuffle);
if ui.checkbox(&mut shuffle, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Allow Spread to Shuffle"))).clicked() {
let _ = app.run("layout.spreadShuffle", json!({"spread": si, "allow": shuffle}));
if item(ui, if pages_in_spread > 1 { "Delete Spread" } else { "Delete Page" }) {
let first = doc.first_page_of_spread(si);
let _ = app.run("layout.pages.delete", json!({"pages": (first..first + pages_in_spread).collect::<Vec<_>>()}));
ui.close();
}
ui.separator();
for (id, label) in [
("layout.overrideParentItems", "Override All Parent Page Items"),
("layout.removeOverrides", "Remove All Local Overrides"),
("layout.detachAll", "Detach All Objects from Parent"),
] {
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() {
let _ = app.run(id, json!({"page": abs}));
ui.close();
}
if item(ui, "New Parent…") {
crate::menus::activate(app, "layout.parents.new", &serde_json::Value::Null);
ui.close();
}
crate::menus::menu_button(ui, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Parent")), |ui| {
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "[None]"))).clicked() {
crate::menus::menu_button(ui, crate::rtl::widget(ui, tr("Apply Parent to Pages")), |ui| {
if ui.button(crate::rtl::widget(ui, tr("[None]"))).clicked() {
let _ = app.run("layout.pages.applyParent", json!({"pages": [abs], "parent": null}));
ui.close();
}
@@ -323,6 +315,45 @@ fn page_menu(app: &mut DesignApp, ui: &mut egui::Ui, doc: &designcraft_doc::Docu
}
}
});
if item(ui, "Override All Parent Page Items") {
let _ = app.run("layout.overrideParentItems", json!({"page": abs}));
ui.close();
}
crate::menus::menu_button(ui, crate::rtl::widget(ui, tr("Parent Pages")), |ui| {
for (id, label) in [("layout.removeOverrides", "Remove All Local Overrides"), ("layout.detachAll", "Detach All Objects from Parent")] {
if ui.button(crate::rtl::widget(ui, tr(label))).clicked() {
let _ = app.run(id, json!({"page": abs}));
ui.close();
}
}
});
ui.separator();
if item(ui, "Create Alternate Layout…") {
crate::menus::activate(app, "layout.createAlternate", &serde_json::Value::Null);
ui.close();
}
if item(ui, "Numbering & Section Options…") {
crate::menus::activate(app, "layout.section", &serde_json::Value::Null);
ui.close();
}
ui.separator();
let mut shuffle = doc.spreads.get(si).is_none_or(|sp| sp.allow_shuffle);
if ui.checkbox(&mut shuffle, crate::rtl::widget(ui, tr("Allow Selected Spread to Shuffle"))).clicked() {
let _ = app.run("layout.spreadShuffle", json!({"spread": si, "allow": shuffle}));
ui.close();
}
// Not in InDesign's menu (it drags pages into spreads): kept for spreads of up to 10 pages.
if si > 0
&& ui
.button(crate::rtl::widget(ui, tr("Add to Previous Spread")))
.on_hover_ui(|ui| {
crate::rtl::label(ui, tr("Spreads of up to 10 pages"));
})
.clicked()
{
let _ = app.run("layout.pages.toSpread", json!({"page": abs + 1, "spread": si - 1}));
ui.close();
}
}
/// A small label following the pointer while dragging.
@@ -370,6 +401,38 @@ mod tests {
h.run_steps(3);
}
#[test]
fn the_panel_menu_follows_indesigns_order() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"pages": 2, "facingPages": false})).unwrap();
let mut h = Harness::builder().with_size(vec2(320.0, 600.0)).build_ui_state(
|ui, app: &mut DesignApp| {
let key = egui::Id::new("test_fonts");
if ui.data(|d| d.get_temp::<bool>(key)).is_none() {
crate::theme::install_fonts(ui.ctx(), "");
ui.data_mut(|d| d.insert_temp(key, true));
return;
}
let doc = app.session.doc().unwrap().doc.clone();
page_menu(app, ui, &doc, 0, 0);
},
app,
);
h.run_steps(3);
// InDesign's items, top to bottom.
let ys: Vec<f32> = ["Insert Pages…", "Move Pages…", "Duplicate Spread", "Duplicate Spread at End of Document", "Delete Page", "New Parent…"]
.iter()
.map(|l| h.get_by_label(l).rect().center().y)
.collect();
assert!(ys.windows(2).all(|w| w[0] < w[1]), "{ys:?}");
h.get_by_label("Duplicate Spread at End of Document").click();
h.run_steps(3);
assert_eq!(h.state().session.doc().unwrap().doc.page_count(), 3);
h.get_by_label("Insert Pages…").click();
h.run_steps(3);
assert!(h.state().ui.dialog.as_ref().is_some_and(|d| d.id == "cmd:layout.pages.insert"));
}
#[test]
fn a_page_dropped_on_create_new_page_is_duplicated_at_the_end() {
// Chris Cicchino's Page Tool video (2:06): "click and drag onto my plus sign I can duplicate that".