diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index 9e6c2d017..361e2f40b 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -1409,17 +1409,21 @@ fn duplicate_layer(s: &mut Session, p: &Value) -> Result { d.insert_item(r, copy, None)?; } // A story copy holds the same anchored objects as the original: they get ids of their own, and the stories of - // anchored text frames get copies in turn. + // anchored text frames get copies in turn. The story copies are taken in story order. let mut pending: Vec = stories.values().copied().collect(); + pending.sort_unstable(); + pending.reverse(); while let Some(new) = pending.pop() { let Some(mut st) = d.story(new).cloned() else { continue }; renumber_anchored(d, &mut st, new_id, &mut stories, &mut ids, &mut pending); d.stories.insert(new, Arc::new(st)); } // Each story copy gets its frames in the original order, text anchors of its own, which its cross-references - // and those of the other copies point at, and tables with ids of their own. + // and those of the other copies point at, and tables with ids of their own, allocated in story order. let mut anchors = HashMap::new(); - for (old, new) in &stories { + let mut order: Vec<(StoryId, StoryId)> = stories.iter().map(|(old, new)| (*old, *new)).collect(); + order.sort_unstable_by_key(|(_, new)| *new); + for (old, new) in &order { let Some(mut st) = d.story(*new).cloned() else { continue }; st.frames = src.story(*old).map(|o| o.frames.iter().filter_map(|f| ids.get(f).copied()).collect()).unwrap_or_default(); for (k, a) in (1..).zip(st.anchors.iter_mut()) { @@ -2116,6 +2120,33 @@ mod layer_tests { assert_eq!(d.story(copied).unwrap().tables[&new].nrows(), 2, "a command naming the copy's table changes the copy"); } + #[test] + fn duplicate_layer_numbers_copied_tables_and_anchored_objects_in_story_order() { + let mut s = crate::Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let base = s.doc().unwrap().doc.layers[0].id.0; + for k in 0..8 { + let y = 72 + 80 * k; + let text = json!({"rect": [72, y, 400, y + 60], "content": "text", "text": "Rows"}); + let f = s.execute("frame.create", &text).unwrap()["id"].as_u64().unwrap(); + let sid = s.doc().unwrap().doc.item(designcraft_doc::ItemId(f)).unwrap().text_frame().unwrap().story; + s.execute("text.select", &json!({"story": sid.0, "anchor": 4, "focus": 4})).unwrap(); + s.execute("table.insert", &json!({"rows": 1, "cols": 2})).unwrap(); + let b = s.execute("frame.create", &json!({"rect": [450, y, 474, y + 40]})).unwrap()["id"].as_u64().unwrap(); + s.execute("anchored.insert", &json!({"ids": [b], "story": sid.0, "pos": 0})).unwrap(); + } + let copy = s.execute("layer.duplicate", &json!({"id": base})).unwrap()["id"].as_u64().unwrap(); + let d = &s.doc().unwrap().doc; + d.check().unwrap(); + let mut stories: Vec<_> = on_layer(d, copy).iter().filter_map(|i| i.text_frame().map(|t| t.story)).collect(); + stories.sort_unstable(); + assert_eq!(stories.len(), 8); + let tables: Vec = stories.iter().map(|sid| *d.story(*sid).unwrap().tables.keys().next().unwrap()).collect(); + let objects: Vec = stories.iter().map(|sid| d.story(*sid).unwrap().objects[0].item.id.0).collect(); + assert!(tables.is_sorted(), "the copied tables take ids in story order: {tables:?}"); + assert!(objects.is_sorted(), "the copied anchored objects take ids in story order: {objects:?}"); + } + /// 40 stories, each holding two anchored text frames threaded in the next: 2^40 paths, 40 stories. #[test] fn duplicate_layer_checks_each_shared_story_once() { diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index d1b462ab5..27f4df308 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -4562,6 +4562,10 @@ const TABLE: &[(&str, [&str; 5])] = &[ "“{name}”的图层选项…", ], ), + ( + "Duplicate Layer \"{name}\"", + ["Ebene „{name}“ duplizieren", "Dupliquer le calque « {name} »", "Duplicar capa «{name}»", "レイヤー「{name}」を複製", "复制图层“{name}”"], + ), ("Swatch Name:", ["Farbfeldname:", "Nom de la nuance :", "Nombre de muestra:", "スウォッチ名:", "色板名称:"]), ( "Name with Color Value", @@ -4968,6 +4972,20 @@ mod tests { } } + #[test] + fn duplicate_layer_menu_item_is_localized_in_every_interface_language() { + let key = "Duplicate Layer \"{name}\""; + for (code, name) in LANGUAGES { + let label = tr(code, key); + if code.is_empty() { + assert_eq!(label, key, "{name}"); + } else { + assert_ne!(label, key, "{name} shows the Layers panel's Duplicate Layer item in English"); + assert!(label.contains("{name}"), "{name}"); + } + } + } + #[test] fn context_captions_come_from_their_context_or_the_plain_translation() { for (lang, table) in CONTEXT_TABLES { diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index f1dbe0acd..6aecde83c 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -1813,6 +1813,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Color Group Options…", "خيارات مجموعة الألوان…"), ("Layer Options", "خيارات الطبقة"), ("Layer Options for \"{name}\"…", "خيارات الطبقة \"{name}\"…"), + ("Duplicate Layer \"{name}\"", "تكرار الطبقة \"{name}\""), ("Swatch Name:", "اسم حامل اللون:"), ("Name with Color Value", "التسمية بقيمة اللون"), ("Color Type:", "نوع اللون:"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 2969d4cff..49b9ae716 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -210,6 +210,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Delete Column", "Odstranit sloupec"), ("Delete Condition", "Odstranit podmínku"), ("Delete Layer", "Odstranit vrstvu"), + ("Duplicate Layer \"{name}\"", "Duplikovat vrstvu „{name}“"), ("Delete Page", "Odstranit stránku"), ("Delete Row", "Odstranit řádek"), ("Delete Swatch", "Odstranit vzorek"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index aaf49bdb3..3ab1df80b 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -2040,6 +2040,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Color Group Options…", "Farbgruppenoptionen …"), ("Layer Options", "Ebenenoptionen"), ("Layer Options for \"{name}\"…", "Ebenenoptionen für „{name}“ …"), + ("Duplicate Layer \"{name}\"", "Ebene „{name}“ duplizieren"), ("Swatch Name:", "Farbfeldname:"), ("Name with Color Value", "Name mit Farbwert"), ("Color Type:", "Farbtyp:"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index db671bc83..cd68c8a53 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -1865,6 +1865,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Color Group Options…", "Opzioni gruppo colori…"), ("Layer Options", "Opzioni livello"), ("Layer Options for \"{name}\"…", "Opzioni livello \"{name}\"…"), + ("Duplicate Layer \"{name}\"", "Duplica livello \"{name}\""), ("Swatch Name:", "Nome campione:"), ("Name with Color Value", "Nome con valore colore"), ("Color Type:", "Tipo colore:"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 01814f271..abeca545d 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -2123,6 +2123,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Color Group Options…", "カラーグループオプション…"), ("Layer Options", "レイヤーオプション"), ("Layer Options for \"{name}\"…", "「{name}」のレイヤーオプション…"), + ("Duplicate Layer \"{name}\"", "レイヤー「{name}」を複製"), ("Swatch Name:", "スウォッチ名:"), ("Name with Color Value", "カラー値を名前にする"), ("Color Type:", "カラータイプ:"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index e4136880b..ebd613805 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -2155,6 +2155,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Last commit", "ბოლო კომიტი"), ("Layer Options", "შრის პარამეტრები"), ("Layer Options for \"{name}\"…", "შრის პარამეტრები: „{name}“…"), + ("Duplicate Layer \"{name}\"", "შრის დუბლირება: „{name}“"), ("Leader:", "მიმთითებელი:"), ("Leading Model:", "სტრიქონთაშორისი ინტერვალის მოდელი:"), ("Left-aligned tab", "მარცხნივ სწორებული ტაბულაცია"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index bd3d96b9a..dfae05867 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -211,6 +211,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Delete Column", "Usuń kolumnę"), ("Delete Condition", "Usuń warunek"), ("Delete Layer", "Usuń warstwę"), + ("Duplicate Layer \"{name}\"", "Powiel warstwę „{name}”"), ("Delete Page", "Usuń stronę"), ("Delete Row", "Usuń wiersz"), ("Delete Swatch", "Usuń próbkę"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index d8125cfe4..84f61b9ea 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -1417,6 +1417,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Color Group Options…", "Opções do grupo de cores…"), ("Layer Options", "Opções de camada"), ("Layer Options for \"{name}\"…", "Opções da camada \"{name}\"…"), + ("Duplicate Layer \"{name}\"", "Duplicar camada \"{name}\""), ("Swatch Name:", "Nome da amostra:"), ("Name with Color Value", "Nome com valor da cor"), ("Color Type:", "Tipo de cor:"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 5e8b58806..ffcb2d8c8 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -210,6 +210,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Delete Column", "Удалить столбец"), ("Delete Condition", "Удалить условие"), ("Delete Layer", "Удалить слой"), + ("Duplicate Layer \"{name}\"", "Дублировать слой «{name}»"), ("Delete Page", "Удалить страницу"), ("Delete Row", "Удалить строку"), ("Delete Swatch", "Удалить образец"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index f0cf41017..8725858ba 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -1833,6 +1833,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Color Group Options…", "Параметри групи кольорів…"), ("Layer Options", "Параметри шару"), ("Layer Options for \"{name}\"…", "Параметри шару «{name}»…"), + ("Duplicate Layer \"{name}\"", "Дублювати шар «{name}»"), ("Swatch Name:", "Назва зразка:"), ("Name with Color Value", "Назва за значенням кольору"), ("Color Type:", "Тип кольору:"),