Merge #870: Pages panel menu order

# Conflicts:
#	crates/ui-egui/src/i18n.rs
This commit is contained in:
aiongg
2026-10-14 01:38:57 +00:00
9 changed files with 142 additions and 29 deletions
+1
View File
@@ -128,6 +128,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-13** · Control-channel clicks and drags validate positive counts and the complete pending-event budget before queuing input (#732). Oversized requests leave pending gestures intact, preventing unbounded click-count allocation. No readiness percentage change.
- **2026-10-13** · Switching between document and parent pages refreshes the canvas immediately, preventing black parent pages and stale document objects when the zoom stays the same. Ordinary edits retain partial repainting. No readiness percentage change.
+22
View File
@@ -684,6 +684,28 @@ const TABLE: &[(&str, [&str; 5])] = &[
("Document Setup", ["Document Setup", "Format de document", "Document Setup", "Document Setup", "文档设置"]),
("Drop Shadow", ["Drop Shadow", "Ombre portée", "Drop Shadow", "Drop Shadow", "投影"]),
("Duplicate Spread", ["Duplicate Spread", "Dupliquer la planche", "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", "E-mail", "Email", "Email", "电子邮件"]),
("Embed", ["Embed", "Incorporer", "Embed", "Embed", "嵌入"]),
("Export PDF", ["PDF exportieren", "Exporter en PDF", "Exportar PDF", "PDF を書き出し", "导出PDF"]),
+4
View File
@@ -921,6 +921,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
@@ -222,6 +222,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
@@ -342,6 +342,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
@@ -916,6 +916,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
@@ -223,6 +223,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
@@ -646,7 +646,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
@@ -336,47 +336,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();
}
@@ -388,6 +380,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.
@@ -476,6 +507,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".