diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 45159ef9c..b5efe59b0 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -380,7 +380,9 @@ fn undo(s: &mut Session, _: &Value) -> Result { let doc = std::mem::replace(&mut st.doc, e.doc); let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection)); let selection = shared_selection(selection, st.history.redo.last()); - st.history.redo.push(crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs)); + let mut turned = crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs); + turned.named = e.named; + st.history.redo.push(turned); st.history.trim(); st.revision += 1; st.selection.prune(&st.doc); @@ -403,7 +405,9 @@ fn redo(s: &mut Session, _: &Value) -> Result { let doc = std::mem::replace(&mut st.doc, e.doc); let selection = std::mem::replace(&mut st.selection, Arc::unwrap_or_clone(e.selection)); let selection = shared_selection(selection, st.history.undo.last()); - st.history.undo.push(crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs)); + let mut turned = crate::HistoryEntry::turned(e.label, doc, selection, &st.doc, costs); + turned.named = e.named; + st.history.undo.push(turned); st.history.trim(); st.revision += 1; st.selection.prune(&st.doc); diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index d33431441..5f796bcf2 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -456,7 +456,7 @@ fn style_redefine(s: &mut Session, p: &Value) -> Result { let look = GraphicStyle::of(String::new(), captured(n), n); let name = d.graphic_styles[i].name.clone(); let chars = s.prefs.override_char_color; - s.edit(&format!("Redefine Graphic Style \u{201c}{name}\u{201d}"), |d, _| { + s.edit_named("Redefine Graphic Style “{name}”", &name, false, |d, _| { let id = d.graphic_style_id(i); let old = std::mem::replace(&mut d.graphic_styles[i], GraphicStyle { name: name.clone(), id, ..look }); // Objects still linked follow the new definition; edited ones are unlinked (they keep diff --git a/crates/engine/src/cmd/textstyles.rs b/crates/engine/src/cmd/textstyles.rs index 7b047f23d..c6303405f 100644 --- a/crates/engine/src/cmd/textstyles.rs +++ b/crates/engine/src/cmd/textstyles.rs @@ -442,7 +442,7 @@ fn new(s: &mut Session, p: &Value, kind: Kind) -> Result { return Err(bad(&c, format!("a style named `{name}` already exists"))); } let n2 = name.clone(); - s.edit(&format!("New {base}"), |d, _| { + s.edit(if kind == Kind::Char { "New Character Style" } else { "New Paragraph Style" }, |d, _| { kind.defs_mut(d).push(TextStyleDef { name: n2, attrs }); Ok(()) })?; @@ -478,7 +478,7 @@ fn apply(s: &mut Session, p: &Value, kind: Kind) -> Result { Ok(st) }; let count = ids.len(); - s.edit(&format!("Apply {name}"), |d, _| { + s.edit_named("Apply {name}", &name, name == kind.normal(), |d, _| { for id in &ids { let Some(t) = text_mut(d, *id) else { continue }; match kind { @@ -528,7 +528,7 @@ fn redefine_to(s: &mut Session, kind: Kind, name: &str, new_attrs: Map def.attrs = new_attrs.clone(), None => kind.defs_mut(d).push(TextStyleDef { name: name.clone(), attrs: new_attrs.clone() }), @@ -664,6 +664,37 @@ mod tests { assert_eq!(list["styles"][1]["uses"], 1); } + /// Applying and redefining a style record a step named from a template with the style's name + /// in it (`HistoryEntry::named`), and undo and redo keep it. The Normal styles' names are + /// marked as names VectorCraft gives. A new style's step has a name of its own. + #[test] + fn style_steps_are_named_from_a_template() { + let (mut s, id) = session_with_text("Hello"); + let last = |s: &Session| { + let e = s.doc().unwrap().history.undo.last().unwrap(); + (e.label.clone(), e.named().map(|n| (n.template, n.name.clone(), n.built_in))) + }; + let named = |template, name: &str, built_in| Some((template, name.to_string(), built_in)); + s.execute("charStyle.new", &json!({"name": "Body"})).unwrap(); + assert_eq!(last(&s), ("New Character Style".into(), None)); + s.execute("charStyle.apply", &json!({"name": "Body", "id": id})).unwrap(); + assert_eq!(last(&s), ("Apply Body".into(), named("Apply {name}", "Body", false))); + s.execute("edit.undo", &json!({})).unwrap(); + let redo = s.doc().unwrap().history.redo.last().unwrap().named().cloned(); + assert_eq!(redo.map(|n| n.label()).as_deref(), Some("Apply Body")); + s.execute("edit.redo", &json!({})).unwrap(); + assert_eq!(last(&s), ("Apply Body".into(), named("Apply {name}", "Body", false))); + s.execute("charStyle.apply", &json!({"name": NORMAL_CHAR, "id": id})).unwrap(); + assert_eq!(last(&s), (format!("Apply {NORMAL_CHAR}"), named("Apply {name}", NORMAL_CHAR, true))); + s.execute("paraStyle.new", &json!({"name": "Lead"})).unwrap(); + assert_eq!(last(&s), ("New Paragraph Style".into(), None)); + s.execute("paraStyle.setAttrs", &json!({"name": "Lead", "attrs": {"direction": "rightToLeft"}})).unwrap(); + assert_eq!(last(&s), ("Redefine Lead".into(), named("Redefine {name}", "Lead", false))); + // A step renamed since it was recorded is no longer named from the template. + s.doc_mut().unwrap().history.undo.last_mut().unwrap().label = "Typing".into(); + assert_eq!(last(&s), ("Typing".into(), None)); + } + #[test] fn new_from_selection_rename_delete_and_undo() { let (mut s, id) = session_with_text("Title"); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index b02ebafb2..50ed6976e 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -91,6 +91,27 @@ pub struct HistoryEntry { next_bytes: usize, /// Which document the figures were taken against (its address): the next state. next: usize, + /// The template and name `label` was made from ([`Session::edit_named`]). + named: Option, +} + +/// A step name made by filling the `{name}` placeholder of a template with a name +/// ([`Session::edit_named`]). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NamedStep { + /// The English template, with one `{name}` placeholder (`Apply {name}`). + pub template: &'static str, + /// The name in the placeholder, as the document holds it (a style's name). + pub name: String, + /// The name is one VectorCraft gives (`[Normal Character Style]`), not one the user typed. + pub built_in: bool, +} + +impl NamedStep { + /// The step name: the template with the name in its placeholder. + pub fn label(&self) -> String { + self.template.replace("{name}", &self.name) + } } impl HistoryEntry { @@ -98,7 +119,14 @@ impl HistoryEntry { pub(crate) fn kept(label: String, doc: Arc, selection: Arc, next: &Document) -> Self { doc.drop_lookup_cache(); let (bytes, next_bytes) = doc.retained_bytes_each(next); - HistoryEntry { label, doc, selection, bytes, next_bytes, next: std::ptr::from_ref(next) as usize } + HistoryEntry { label, doc, selection, bytes, next_bytes, next: std::ptr::from_ref(next) as usize, named: None } + } + + /// The template and name the step's label was made from, while the label is still the one + /// they make: `None` for a step named any other way, and for one whose label was changed + /// since it was recorded. + pub fn named(&self) -> Option<&NamedStep> { + self.named.as_ref().filter(|n| n.label() == self.label) } /// The step that undoing (redoing) the step `costs` came from makes: it keeps `doc`, the @@ -112,7 +140,7 @@ impl HistoryEntry { return Self::kept(label, doc, selection, now); } doc.drop_lookup_cache(); - HistoryEntry { label, doc, selection, bytes: costs.next_bytes, next_bytes: costs.bytes, next: Arc::as_ptr(now) as usize } + HistoryEntry { label, doc, selection, bytes: costs.next_bytes, next_bytes: costs.bytes, next: Arc::as_ptr(now) as usize, named: None } } /// The figures [`HistoryEntry::turned`] reuses. @@ -1739,6 +1767,33 @@ impl Session { } } + /// [`Session::edit`] with the step named `template`, its `{name}` placeholder filled with `name` + /// (a style's name). The undo step it records keeps the template and the name + /// ([`HistoryEntry::named`]), and a frontend shows the template's translation with the name in + /// it. An edit that records no step of its own (inside an interaction, or a later edit of an + /// undo group) keeps neither, and neither does a step an undo group names. + pub fn edit_named( + &mut self, + template: &'static str, + name: &str, + built_in: bool, + f: impl FnOnce(&mut Document, &mut Selection) -> Result, + ) -> Result { + let named = NamedStep { template, name: name.to_string(), built_in }; + let step = named.label(); + // The step this edit records keeps the document as it is now. + let now = Arc::as_ptr(&self.doc()?.doc); + let v = self.edit(&step, f)?; + if let Ok(st) = self.doc_mut() + && let Some(e) = st.history.undo.last_mut() + && Arc::as_ptr(&e.doc) == now + && e.label == step + { + e.named = Some(named); + } + Ok(v) + } + /// Change only the selection (not an undo step). pub fn select(&mut self, f: impl FnOnce(&Document, &mut Selection)) -> Result<()> { self.active_appearance_item = None; diff --git a/crates/engine/src/tests_styles.rs b/crates/engine/src/tests_styles.rs index faa05f3a1..8c0ff4e76 100644 --- a/crates/engine/src/tests_styles.rs +++ b/crates/engine/src/tests_styles.rs @@ -175,6 +175,10 @@ fn redefine_updates_every_linked_object() { let blue = Paint::solid(Color::from_hex("#0000ff").unwrap()); assert_eq!((fill(&s, b), fill(&s, c)), (blue.clone(), blue)); assert_eq!(linked(&mut s, "S"), vec![a.0, b.0, c.0]); + // The step is named from a template with the style's name in it. + let step = s.doc().unwrap().history.undo.last().unwrap().clone(); + let named = step.named().map(|n| (n.template, n.name.as_str(), n.built_in)); + assert_eq!((step.label.as_str(), named), ("Redefine Graphic Style “S”", Some(("Redefine Graphic Style “{name}”", "S", false)))); run(&mut s, "edit.undo", json!({})); assert_eq!(fill(&s, b), Paint::solid(Color::WHITE)); } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index d4cfa8a87..c2412831a 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2398,6 +2398,8 @@ Redefine Character Style Предефиниране на стил на знаци Redefine Graphic Style Предефиниране на графичен стил Redefine Graphic Style “{name}” Предефиниране на графичен стил „{name}“ + Apply {name} Прилагане на стил „{name}“ + Redefine {name} Предефиниране на стил „{name}“ Redefine Paragraph Style Предефиниране на стил на абзац Redefine Symbol Предефиниране на символ Redefine {kind} Style Предефиниране на стил {kind} diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index fb91ed057..df1e83794 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1328,6 +1328,8 @@ lighting Light Světlo Opacity: {value} Krytí: {value} Rectangle: Sunshine Obdélník: Sluneční svit Redefine Graphic Style “{name}” Předefinovat grafický styl „{name}“ + Apply {name} Použít styl „{name}“ + Redefine {name} Předefinovat styl „{name}“ Show Thumbnail Zobrazit miniaturu Delete Artboards Odstranit pracovní plochy Delete Empty Artboards Odstranit prázdné pracovní plochy diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 6d0573712..fde8d7207 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1249,6 +1249,8 @@ lighting Light Lys Opacity: {value} Opacitet: {value} Rectangle: Sunshine Rektangel: Sunshine Redefine Graphic Style “{name}” Omdefinér grafisk typografi »{name}« + Apply {name} Anvend stilen »{name}« + Redefine {name} Omdefinér stilen »{name}« Show Thumbnail Vis miniatur Artboard Options: edit with the Artboard tool Tegnebrætindstillinger: redigér med tegnebrætværktøjet Delete Artboards Slet tegnebrætter diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 36dfbc0dc..82909c8b0 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2423,6 +2423,8 @@ mark-style Japanese Japanisch Redefine Character Style Zeichenformat neu definieren Redefine Graphic Style Grafikstil neu definieren Redefine Graphic Style “{name}” Grafikstil „{name}“ neu definieren + Apply {name} Format „{name}“ anwenden + Redefine {name} Format „{name}“ neu definieren Redefine Paragraph Style Absatzformat neu definieren Redefine Symbol Symbol neu definieren Redefine {kind} Style {kind}format neu definieren diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 5fe22d83e..ae1df0e6a 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2968,6 +2968,8 @@ theme Appearance Εμφάνιση Redefine Character Style Επαναπροσδιορισμός στυλ χαρακτήρων Redefine Graphic Style Επαναπροσδιορισμός στυλ γραφικών Redefine Graphic Style “{name}” Επαναπροσδιορισμός στυλ γραφικών «{name}» + Apply {name} Εφαρμογή στυλ «{name}» + Redefine {name} Επαναπροσδιορισμός στυλ «{name}» Redefine Paragraph Style Επαναπροσδιορισμός στυλ παραγράφου Redefine Symbol Επαναπροσδιορισμός συμβόλου Redefine {kind} Style Επαναπροσδιορισμός στυλ {kind} diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 39692f99f..e4b3aeb3a 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2444,6 +2444,8 @@ Redefine Character Style Redefinir estilo de carácter Redefine Graphic Style Redefinir estilo gráfico Redefine Graphic Style “{name}” Redefinir estilo gráfico “{name}” + Apply {name} Aplicar estilo “{name}” + Redefine {name} Redefinir estilo “{name}” Redefine Paragraph Style Redefinir estilo de párrafo Redefine Symbol Redefinir símbolo Redefine {kind} Style Redefinir estilo de {kind} diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 71aa3e03c..b9123bf19 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1302,6 +1302,8 @@ lighting Light Valo Opacity: {value} Peittävyys: {value} Rectangle: Sunshine Suorakulmio: Auringonpaiste Redefine Graphic Style “{name}” Määritä grafiikkatyyli ”{name}” uudelleen + Apply {name} Käytä tyyliä ”{name}” + Redefine {name} Määritä tyyli ”{name}” uudelleen Show Thumbnail Näytä pikkukuva Artboard Options: edit with the Artboard tool Piirtoalustan asetukset: muokkaa piirtoalustatyökalulla Delete Artboards Poista piirtoalustat diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index fb08541ad..43b5a6b90 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2441,6 +2441,8 @@ Redefine Character Style Redéfinir le style de caractère Redefine Graphic Style Redéfinir le style graphique Redefine Graphic Style “{name}” Redéfinir le style graphique « {name} » + Apply {name} Appliquer le style « {name} » + Redefine {name} Redéfinir le style « {name} » Redefine Paragraph Style Redéfinir le style de paragraphe Redefine Symbol Redéfinir le symbole Redefine {kind} Style Redéfinir le style de {kind} diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index c57346a35..750b82c11 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2979,6 +2979,8 @@ mark-style Japanese Japán Redefine Character Style Karakterstílus újradefiniálása Redefine Graphic Style Grafikai stílus újradefiniálása Redefine Graphic Style “{name}” Grafikai stílus újradefiniálása: „{name}” + Apply {name} Stílus alkalmazása: „{name}” + Redefine {name} Stílus újradefiniálása: „{name}” Redefine Paragraph Style Bekezdésstílus újradefiniálása Redefine Symbol Szimbólum újradefiniálása Redefine {kind} Style Stílus újradefiniálása: {kind} diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 415b6e64b..5d3670a68 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1249,6 +1249,8 @@ lighting Light Cahaya Opacity: {value} Opasitas: {value} Rectangle: Sunshine Persegi panjang: Sunshine Redefine Graphic Style “{name}” Definisikan ulang gaya grafis “{name}” + Apply {name} Terapkan gaya “{name}” + Redefine {name} Definisikan ulang gaya “{name}” Show Thumbnail Tampilkan thumbnail Artboard Options: edit with the Artboard tool Opsi artboard: edit dengan alat artboard Delete Artboards Hapus artboard diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 909a71356..7deda3fbc 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2474,6 +2474,8 @@ mark-style Japanese Giapponese Redefine Character Style Ridefinisci stile carattere Redefine Graphic Style Ridefinisci stile grafico Redefine Graphic Style “{name}” Ridefinisci stile grafico “{name}” + Apply {name} Applica stile “{name}” + Redefine {name} Ridefinisci stile “{name}” Redefine Paragraph Style Ridefinisci stile paragrafo Redefine Symbol Ridefinisci simbolo Redefine {kind} Style Ridefinisci stile {kind} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 2dfebfa7b..d65925900 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2456,6 +2456,8 @@ mark-style Japanese 日本式 Redefine Character Style 文字スタイルを再定義 Redefine Graphic Style グラフィックスタイルを再定義 Redefine Graphic Style “{name}” グラフィックスタイル「{name}」を再定義 + Apply {name} スタイル「{name}」を適用 + Redefine {name} スタイル「{name}」を再定義 Redefine Paragraph Style 段落スタイルを再定義 Redefine Symbol シンボルを再定義 Redefine {kind} Style {kind}スタイルを再定義 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index c01be87bf..8dad6d155 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2450,6 +2450,8 @@ mark-style Japanese 일본식 Redefine Character Style 문자 스타일 재정의 Redefine Graphic Style 그래픽 스타일 재정의 Redefine Graphic Style “{name}” 그래픽 스타일 “{name}” 재정의 + Apply {name} 스타일 “{name}” 적용 + Redefine {name} 스타일 “{name}” 재정의 Redefine Paragraph Style 단락 스타일 재정의 Redefine Symbol 심볼 재정의 Redefine {kind} Style {kind} 스타일 재정의 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 4c5d226f1..c7b1d571d 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1263,6 +1263,8 @@ lighting Light Licht Opacity: {value} Dekking: {value} Rectangle: Sunshine Rechthoek: Sunshine Redefine Graphic Style “{name}” Grafische stijl „{name}” opnieuw definiëren + Apply {name} Stijl „{name}” toepassen + Redefine {name} Stijl „{name}” opnieuw definiëren Show Thumbnail Miniatuur tonen Artboard Options: edit with the Artboard tool Opties voor tekengebied: bewerken met het Tekengebied-gereedschap Delete Artboards Tekengebieden verwijderen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index d5eb7372f..d0780fa7a 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1242,6 +1242,8 @@ lighting Light Światło Opacity: {value} Krycie: {value} Rectangle: Sunshine Prostokąt: Słońce Redefine Graphic Style “{name}” Zdefiniuj ponownie styl graficzny „{name}” + Apply {name} Zastosuj styl „{name}” + Redefine {name} Zdefiniuj ponownie styl „{name}” Show Thumbnail Pokaż miniaturę Delete Artboards Usuń obszary kompozycji Delete Empty Artboards Usuń puste obszary kompozycji diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 4f7602b5a..d8c50174e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2031,6 +2031,8 @@ Redefine Character Style Redefinir Estilo de Caractere Redefine Graphic Style Redefinir Estilo Gráfico Redefine Graphic Style “{name}” Redefinir Estilo Gráfico “{name}” + Apply {name} Aplicar Estilo “{name}” + Redefine {name} Redefinir Estilo “{name}” Redefine Paragraph Style Redefinir Estilo de Parágrafo Redefine Symbol Redefinir Símbolo Redefine {kind} Style Redefinir Estilo de {kind} diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 7ab19d0cf..fab284b8e 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2470,6 +2470,8 @@ mark-style Japanese Японский Redefine Character Style Переопределить стиль символа Redefine Graphic Style Переопределить графический стиль Redefine Graphic Style “{name}” Переопределить графический стиль «{name}» + Apply {name} Применить стиль «{name}» + Redefine {name} Переопределить стиль «{name}» Redefine Paragraph Style Переопределить стиль абзаца Redefine Symbol Переопределить символ Redefine {kind} Style Переопределить стиль {kind} diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 7b4b7d852..f65776215 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2398,6 +2398,8 @@ Redefine Character Style Definiera om teckenstil Redefine Graphic Style Definiera om grafikstil Redefine Graphic Style “{name}” Definiera om grafikstilen ”{name}” + Apply {name} Använd stilen ”{name}” + Redefine {name} Definiera om stilen ”{name}” Redefine Paragraph Style Definiera om styckestil Redefine Symbol Definiera om symbol Redefine {kind} Style Definiera om {kind}stil diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index 4b09364df..2b437b9b3 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -440,18 +440,28 @@ fn every_tl_literal_is_translated() { } } -/// Every undo step name the engine, the tools and the shell record ([`undo_labels`]), and every -/// effect's name without its ellipsis, has a row in each language marked `complete_menus`: its plain -/// row, or the row of the same name with an ellipsis (`panels::history::step_name`). +/// Every undo step name the engine, the tools and the shell record ([`undo_labels`]), every +/// effect's name without its ellipsis, and the Liquify tools' step names (`{} Tool`), has a row in +/// each language marked `complete_menus`: its plain row, or the row of the same name with an +/// ellipsis (`panels::history::step_name`). Every template a step name is made from with +/// `Session::edit_named` has a plain row in each, which `panels::history::entry_name` fills with the +/// name. No other step name is built with `format!`. #[test] fn every_undo_label_is_translated() { - let mut labels = undo_labels(); + let scan = undo_scan(); + let mut labels = scan.labels; assert!(labels.len() > 300, "scan found only {} undo labels", labels.len()); labels.extend(vectorcraft_effects::effect_catalog().iter().map(|e| e.label.trim_end_matches('…').to_string())); + let liquify = ["warp", "twirl", "pucker", "bloat", "scallop", "crystallize", "wrinkle"]; + labels.extend(liquify.iter().filter_map(|k| vectorcraft_tools::distort::liquify::LiquifyKind::parse(k)).map(|k| format!("{} Tool", k.label()))); + assert_eq!(scan.formatted.iter().collect::>(), ["{} Tool"], "step names built with format! (make them with Session::edit_named)"); + assert!(scan.templates.len() >= 3, "scan found only the templates {:?}", scan.templates); for l in LANGUAGES.iter().filter(|l| l.complete_menus) { let lang = Lang::from_code(l.code).expect("registered"); let missing: Vec<_> = labels.iter().filter(|s| crate::panels::history::step_name(lang, s).is_none()).collect(); assert!(missing.is_empty(), "{}: {} untranslated undo labels: {missing:#?}", l.code, missing.len()); + let missing: Vec<_> = scan.templates.iter().filter(|t| l.catalog().plain(t).is_none()).collect(); + assert!(missing.is_empty(), "{}: {} untranslated undo label templates: {missing:#?}", l.code, missing.len()); } } @@ -494,6 +504,9 @@ const UNDO_CRATES: &[&str] = &["engine", "tools", "ui-egui"]; /// Calls whose first argument names an undo step. const UNDO_MARKERS: &[&str] = &[".edit(", "begin_interaction(", "Action::Begin(", "begin_named_undo_group(Some("]; +/// The call whose first argument is the template of an undo step's name (`Session::edit_named`). +const NAMED_MARKER: &str = ".edit_named("; + /// A function in a source file, as [`undo_labels`] reads it. struct SourceFn<'a> { file: usize, @@ -513,8 +526,26 @@ struct SourceFn<'a> { /// `…label` (`pf_label(op)`, `kind.label()`) gives the literals that function returns in the same /// file. A name built with `format!` or taken from data (an effect's, a plug-in's) is not read. pub fn undo_labels() -> std::collections::BTreeSet { + undo_scan().labels +} + +/// What [`undo_scan`] finds in [`UNDO_CRATES`]. +struct UndoScan { + /// The step names written as literals ([`undo_labels`]). + labels: std::collections::BTreeSet, + /// The templates step names are made from with `Session::edit_named` (its first argument). + templates: std::collections::BTreeSet, + /// The format strings of the step names built with `format!` where [`undo_labels`] reads a + /// name. + formatted: std::collections::BTreeSet, +} + +/// The undo step names and templates in [`UNDO_CRATES`] (see [`undo_labels`]). +fn undo_scan() -> UndoScan { let crates = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); let mut out = std::collections::BTreeSet::new(); + let mut templates = std::collections::BTreeSet::new(); + let mut formatted = std::collections::BTreeSet::new(); for krate in UNDO_CRATES { let files = undo_sources(&crates.join(krate).join("src")); let fns: Vec = files.iter().enumerate().flat_map(|(i, t)| source_fns(i, t)).collect(); @@ -551,10 +582,14 @@ pub fn undo_labels() -> std::collections::BTreeSet { } } for (file, e) in exprs { - label_literals(e, file, &fns, &mut out); + label_literals(e, file, &fns, &mut out, &mut formatted); + } + for t in &files { + let named = t.match_indices(NAMED_MARKER).filter_map(|(at, _)| arguments(&t[at + NAMED_MARKER.len()..]).first().copied()); + templates.extend(named.filter_map(|a| a.strip_prefix('"').and_then(string_literal))); } } - out + UndoScan { labels: out, templates, formatted } } /// The sources of a crate's `src` directory, test modules and files and `//` comments aside. @@ -740,10 +775,18 @@ fn bare(e: &str) -> &str { e } -/// Add the step names expression `e` in source file `file` gives (see [`undo_labels`]). -fn label_literals(e: &str, file: usize, fns: &[SourceFn], out: &mut std::collections::BTreeSet) { +/// Add the step names expression `e` in source file `file` gives (see [`undo_labels`]) to `out`, +/// and the format string of a name it builds with `format!` to `formatted`. +fn label_literals( + e: &str, + file: usize, + fns: &[SourceFn], + out: &mut std::collections::BTreeSet, + formatted: &mut std::collections::BTreeSet, +) { let mut e = bare(e); - if e.starts_with("format!(") { + if let Some(args) = e.strip_prefix("format!(") { + formatted.extend(args.trim_start().strip_prefix('"').and_then(string_literal)); return; } if let Some(body) = e.strip_prefix('|').and_then(|r| r.find('|').map(|n| &r[n + 1..])) { @@ -798,6 +841,7 @@ fn dump_source_strings() { let mut all = crate::menus::menu_strings(); all.extend(tl_literals()); all.extend(undo_labels()); + all.extend(undo_scan().templates); let text: String = all.iter().map(|s| format!("{}\n", s.replace('\\', "\\\\").replace('\n', "\\n").replace('\t', "\\t"))).collect(); std::fs::write(path, text).expect("write dump"); } diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7ed0d0975..2f04dbd8c 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2466,6 +2466,8 @@ mark-style Japanese Японський Redefine Character Style Перевизначити стиль символу Redefine Graphic Style Перевизначити графічний стиль Redefine Graphic Style “{name}” Перевизначити графічний стиль «{name}» + Apply {name} Застосувати стиль «{name}» + Redefine {name} Перевизначити стиль «{name}» Redefine Paragraph Style Перевизначити стиль абзацу Redefine Symbol Перевизначити символ Redefine {kind} Style Перевизначити стиль {kind} diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index a67b98f5b..4b79f9ed9 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2756,6 +2756,8 @@ mark-style Japanese Kiểu Nhật Redefine Character Style Xác định lại kiểu ký tự Redefine Graphic Style Xác định lại kiểu đồ họa Redefine Graphic Style “{name}” Xác định lại kiểu đồ họa “{name}” + Apply {name} Áp dụng kiểu “{name}” + Redefine {name} Xác định lại kiểu “{name}” Redefine Paragraph Style Xác định lại kiểu đoạn văn Redefine Symbol Xác định lại ký hiệu Redefine {kind} Style Xác định lại kiểu {kind} diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 5ac52e2b2..542f80ba1 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2362,6 +2362,8 @@ mark-style Japanese 日式 Redefine Character Style 重新定义字符样式 Redefine Graphic Style 重新定义图形样式 Redefine Graphic Style “{name}” 重新定义图形样式“{name}” + Apply {name} 应用样式“{name}” + Redefine {name} 重新定义样式“{name}” Redefine Paragraph Style 重新定义段落样式 Redefine Symbol 重新定义符号 Redefine {kind} Style 重新定义{kind}样式 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 866f28e1c..a5e1acf1d 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2448,6 +2448,8 @@ mark-style Japanese 日式 Redefine Character Style 重新定義字元樣式 Redefine Graphic Style 重新定義繪圖樣式 Redefine Graphic Style “{name}” 重新定義繪圖樣式「{name}」 + Apply {name} 套用樣式「{name}」 + Redefine {name} 重新定義樣式「{name}」 Redefine Paragraph Style 重新定義段落樣式 Redefine Symbol 重新定義符號 Redefine {kind} Style 重新定義{kind}樣式 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 56c87ab24..5e2cef79a 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1909,8 +1909,8 @@ pub fn display_label(app: &VectorcraftApp, id: &str, label: &str) -> String { // The step's name as the History panel lists it. let name = crate::panels::history::shown; match last { - Some(h) if id == "edit.undo" => fmt(tl!("Undo {name}"), &[("name", &name(&h.label))]), - Some(h) => fmt(tl!("Redo {name}"), &[("name", &name(&h.label))]), + Some(h) if id == "edit.undo" => fmt(tl!("Undo {name}"), &[("name", &name(h))]), + Some(h) => fmt(tl!("Redo {name}"), &[("name", &name(h))]), None => tl!(if id == "edit.undo" { "Undo" } else { "Redo" }).to_string(), } } @@ -4295,6 +4295,22 @@ mod tests { app } + /// Edit › Undo and Edit › Redo name a step named after a style as the History panel lists it: + /// the template's translation with the style's name in it. + #[test] + fn undo_and_redo_name_a_style_step_around_the_style_name() { + let mut app = app_with_a_rectangle(); + let text = app.run("text.create", json!({"x": 0, "y": 40, "text": "Hello"})).unwrap()["id"].clone(); + app.run("charStyle.new", json!({"name": "Brush"})).unwrap(); + app.run("charStyle.apply", json!({"name": "Brush", "id": text})).unwrap(); + crate::i18n::set_current(crate::i18n::Lang::from_code("ja").expect("ja registered")); + assert_eq!(display_label(&app, "edit.undo", "Undo"), "スタイル「Brush」を適用を取り消す"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(display_label(&app, "edit.redo", "Redo"), "スタイル「Brush」を適用をやり直す"); + crate::i18n::set_current(crate::i18n::Lang::EN); + assert_eq!(display_label(&app, "edit.redo", "Redo"), "Redo Apply Brush"); + } + #[test] fn save_selection_asks_for_a_name() { let mut app = app_with_a_rectangle(); diff --git a/crates/ui-egui/src/panels/history.rs b/crates/ui-egui/src/panels/history.rs index adc1d6f44..cca14bc9d 100644 --- a/crates/ui-egui/src/panels/history.rs +++ b/crates/ui-egui/src/panels/history.rs @@ -24,10 +24,23 @@ pub fn step_name(lang: crate::i18n::Lang, label: &str) -> Option { has(lang, &dotted).then(|| tr(lang, &dotted).trim_end_matches('…').to_string()) } +/// A step's name in `lang`. A name made from a template (`Apply {name}`, +/// [`vectorcraft_engine::HistoryEntry::named`]) takes the template's plain row with the name in its +/// placeholder. A name VectorCraft gives is shown in `lang`, and any other name is shown as the +/// document holds it. It returns `None` when the catalog has no row for the template. Any other step +/// takes [`step_name`] of its label. +pub fn entry_name(lang: crate::i18n::Lang, h: &vectorcraft_engine::HistoryEntry) -> Option { + use crate::i18n::{fmt, has, label_or_name, tr}; + match h.named() { + Some(n) => has(lang, n.template).then(|| fmt(tr(lang, n.template), &[("name", label_or_name(lang, &n.name, n.built_in))])), + None => step_name(lang, &h.label), + } +} + /// A step's name as the panel lists it: in the UI language, else as recorded. Edit › Undo and Redo /// name the step the same way. -pub(crate) fn shown(label: &str) -> String { - step_name(crate::i18n::current(), label).unwrap_or_else(|| label.to_string()) +pub(crate) fn shown(h: &vectorcraft_engine::HistoryEntry) -> String { + entry_name(crate::i18n::current(), h).unwrap_or_else(|| h.label.clone()) } pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -37,9 +50,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { return; }; let mut rows: Vec<(String, bool)> = vec![(tl!("Open").to_string(), false)]; - rows.extend(st.history.undo.iter().map(|h| (shown(&h.label), false))); + rows.extend(st.history.undo.iter().map(|h| (shown(h), false))); let current = rows.len() - 1; - rows.extend(st.history.redo.iter().rev().map(|h| (shown(&h.label), true))); + rows.extend(st.history.redo.iter().rev().map(|h| (shown(h), true))); let mut steps: Option = None; crate::widgets::list_box(ui, |ui| { ui.spacing_mut().item_spacing.y = 0.0; @@ -100,6 +113,61 @@ mod tests { assert_eq!(steps_to(3, 3), 0); } + /// The step names the panel paints, top to bottom. Without an image loader, each row's icon + /// paints a `⚠` instead, which is left out. + fn listed(app: &mut VectorcraftApp) -> Vec { + let ctx = egui::Context::default(); + let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui)); + out.textures_delta.clear(); + out.shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Text(t) if t.galley.text() != "⚠" => Some(t.galley.text().to_string()), + _ => None, + }) + .collect() + } + + /// A step named after a style lists the template's translation with the style's name in it as + /// the document holds it, also when the name looks like a template, like a step with a row of + /// its own (`Apply Brush`) or like another template's name; the Normal style's name is + /// translated as the Character Styles panel shows it. Undone steps keep their names. + #[test] + fn steps_named_after_a_style_list_the_template_around_the_name() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + let mut run = |id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}")); + run("file.new", json!({"width": 200, "height": 150})); + let rect = run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20}))["id"].clone(); + run("graphicStyle.new", json!({"name": "S", "id": rect})); + run("graphicStyle.redefine", json!({"name": "S", "id": rect})); + let text = run("text.create", json!({"x": 10, "y": 100, "text": "Hello"}))["id"].clone(); + for name in ["Body", "{name}", "Apply", "Brush", "Graphic Style “X”"] { + run("charStyle.new", json!({"name": name})); + } + for name in ["Body", "{name}", "Apply", "Brush", "[Normal Character Style]"] { + run("charStyle.apply", json!({"name": name, "id": text})); + } + run("charStyle.setAttrs", json!({"name": "Graphic Style “X”", "attrs": {"size": 30.0}})); + let ja = crate::i18n::Lang::from_code("ja").expect("ja registered"); + crate::i18n::set_current(ja); + let rows = listed(&mut app); + let want = [ + "グラフィックスタイル「S」を再定義", + "スタイル「Body」を適用", + "スタイル「{name}」を適用", + "スタイル「Apply」を適用", + "スタイル「Brush」を適用", + "スタイル「[標準文字スタイル]」を適用", + "スタイル「Graphic Style “X”」を再定義", + ]; + assert!(rows.contains(&want[0].to_string()), "{rows:#?}"); + assert_eq!(rows[rows.len() - 6..], want[1..], "{rows:#?}"); + app.session.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(listed(&mut app).last().map(String::as_str), Some(want[6]), "an undone step"); + crate::i18n::set_current(crate::i18n::Lang::EN); + assert_eq!(listed(&mut app)[rows.len() - 6..rows.len() - 4], ["Apply Body", "Apply {name}"]); + } + /// A step named after an effect whose catalog row has only the menu label with its ellipsis /// takes that row's translation without the ellipsis; a name with no row has none. #[test] diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ba29c1816..ea467f008 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -461,6 +461,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel inside the window, and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on the dock, which lights up under the pointer, its panels go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, docking a panel into the default tabbed group it doesn't belong to, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set. - The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done. - Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md). + - The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`, `every_undo_label_is_translated`). - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian and Ukrainian ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins). - Edit › Undo and Edit › Redo, in the in-window and native menus, name the step in the interface language as the History panel lists it (`panels::history::step_name`), so when the catalog has no plain row for a step named after an effect, such as Feather, the menu shows the translation of the effect's menu row without its ellipsis (`undo_and_redo_name_the_step_as_the_history_panel_does`). - The History panel translates the undo step names the engine, the tools and the shell write as literals: each has a row in every complete catalog (`every_undo_label_is_translated`), and a step whose catalog row exists only with an ellipsis (such as an effect's menu name) shows that row without the ellipsis.