From a8b3786f8c291d0264a8fcf8b1f9cf626c306720 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 31 Oct 2026 01:07:45 -0700 Subject: [PATCH] Symbols: Delete Symbol asks first, and Alt-click deletes an unused symbol without asking The Symbols panel's Delete Symbol button and menu item now ask before deleting. A symbol no instance uses gets "Delete the selected symbol?" with Yes and No. A symbol with placed instances gets Expand Instances, which turns the instances into ordinary art, Delete Instances, which removes them, and Cancel; either Instances button deletes the symbol in one undo step. Alt-click on the button deletes an unused symbol at once and still asks about one with instances. The question is bound to the document it was asked in: once another document is active, or the symbol is gone, Yes deletes nothing, and a symbol that gained instances while the question was open gets the in-use question instead. symbol.delete itself does not ask. The shared dialog frame takes a Cancel label (DialogSpec::cancel) for the No button. The new strings have rows in every catalog. --- crates/ui-egui/src/dialogs.rs | 9 +- crates/ui-egui/src/dialogs/symbol_delete.rs | 100 ++++++++++ crates/ui-egui/src/i18n/bg.tsv | 6 + crates/ui-egui/src/i18n/bs.tsv | 6 + crates/ui-egui/src/i18n/ca.tsv | 6 + crates/ui-egui/src/i18n/cs.tsv | 6 + crates/ui-egui/src/i18n/da.tsv | 6 + crates/ui-egui/src/i18n/de.tsv | 6 + crates/ui-egui/src/i18n/el.tsv | 6 + crates/ui-egui/src/i18n/es.tsv | 6 + crates/ui-egui/src/i18n/fi.tsv | 6 + crates/ui-egui/src/i18n/fr.tsv | 6 + crates/ui-egui/src/i18n/hu.tsv | 6 + crates/ui-egui/src/i18n/id.tsv | 6 + crates/ui-egui/src/i18n/it.tsv | 6 + crates/ui-egui/src/i18n/ja.tsv | 6 + crates/ui-egui/src/i18n/ka.tsv | 6 + crates/ui-egui/src/i18n/ko.tsv | 6 + crates/ui-egui/src/i18n/nl.tsv | 7 + crates/ui-egui/src/i18n/pl.tsv | 6 + crates/ui-egui/src/i18n/pt-br.tsv | 6 + crates/ui-egui/src/i18n/ru.tsv | 6 + crates/ui-egui/src/i18n/sv.tsv | 6 + crates/ui-egui/src/i18n/tr.tsv | 6 + crates/ui-egui/src/i18n/uk.tsv | 6 + crates/ui-egui/src/i18n/vi.tsv | 6 + crates/ui-egui/src/i18n/zh-hans.tsv | 6 + crates/ui-egui/src/i18n/zh-hant.tsv | 6 + crates/ui-egui/src/panels/symbols.rs | 208 +++++++++++++++++++- docs/target-app-parity.md | 1 + 30 files changed, 471 insertions(+), 4 deletions(-) create mode 100644 crates/ui-egui/src/dialogs/symbol_delete.rs diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 83e7850f4..488cd1d95 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -106,6 +106,7 @@ pub mod spot_colors; pub(crate) mod svg_options; pub mod swatch_conflict; pub mod swatch_options; +pub mod symbol_delete; pub mod symbol_options; pub mod symbolism_options; mod text_export; @@ -175,6 +176,8 @@ pub(crate) struct DialogSpec { pub confirm: fn(&mut VectorcraftApp, &Dialog) -> DialogResult, /// The OK button's label. None: no OK button, Enter does nothing and Cancel reads "Close". pub ok: Option<&'static str>, + /// The Cancel button's label when it is not "Cancel" ("No"). Only with an `ok`. + pub cancel: Option<&'static str>, /// An extra button left of Cancel that sets `discard: true` and confirms ("Don't Save"). pub discard: Option<&'static str>, /// A button left of Cancel, as (label, field), that sets `field: true` and confirms, as OK @@ -217,6 +220,7 @@ impl DialogSpec { Ok(Value::Null) }, ok: Some("OK"), + cancel: None, discard: None, alternate: None, reset: None, @@ -371,6 +375,8 @@ registry! { ShapeBuilderOptions: [shape_builder_options::KIND] => shape_builder_options::SPEC, FlareOptions: [flare_options::KIND] => flare_options::SPEC, SymbolOptions: [symbol_options::KIND] => symbol_options::SPEC, + DeleteSymbol: [symbol_delete::KIND] => symbol_delete::SPEC, + DeleteUsedSymbol: [symbol_delete::USED] => symbol_delete::USED_SPEC, SymbolismOptions: [symbolism_options::KIND] => symbolism_options::SPEC, PerspectiveGridPresets: [perspective_presets::KIND] => perspective_presets::SPEC, PerspectiveGridOptions: [perspective_options::KIND] => perspective_options::SPEC, @@ -397,6 +403,7 @@ pub fn button_labels() -> Vec<&'static str> { let mut v = vec!["Cancel", "Close"]; for spec in std::iter::once(&DialogSpec::FORM).chain(DialogKind::ALL.iter().map(|k| k.spec())) { v.extend(spec.ok); + v.extend(spec.cancel); v.extend(spec.discard); v.extend(spec.alternate.map(|(label, _)| label)); v.extend(spec.reset.map(|_| "Reset")); @@ -638,7 +645,7 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let label = if resets { "Reset" } else if spec.ok.is_some() { - "Cancel" + spec.cancel.unwrap_or("Cancel") } else { "Close" }; diff --git a/crates/ui-egui/src/dialogs/symbol_delete.rs b/crates/ui-egui/src/dialogs/symbol_delete.rs new file mode 100644 index 000000000..730c00cb4 --- /dev/null +++ b/crates/ui-egui/src/dialogs/symbol_delete.rs @@ -0,0 +1,100 @@ +//! The Symbols panel's Delete Symbol question ([`ask`]). A symbol no instance uses gets "Delete the +//! selected symbol?" with Yes and No ([`KIND`]); a symbol with placed instances gets Expand +//! Instances, Delete Instances and Cancel ([`USED`]). +//! +//! Fields: `name` (the symbol) and `doc` (the [`vectorcraft_engine::DocState::uid`] of the document +//! it was asked in). Yes or either Instances button deletes the symbol only while that document is +//! the active one and still has the symbol; an unused symbol that has gained instances since asks +//! the in-use question instead. + +use serde_json::{Value, json}; + +use super::{DialogResult, DialogSpec}; +use crate::VectorcraftApp; +use crate::state::Dialog; + +/// The dialog kind of the question for a symbol no instance uses. +pub const KIND: &str = "deleteSymbol"; + +/// The dialog kind of the question for a symbol with placed instances. +pub const USED: &str = "deleteUsedSymbol"; + +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Delete the selected symbol?").into(), + body: |_, _, _| false, + confirm: confirm_unused, + ok: Some("Yes"), + cancel: Some("No"), + max_width: Some(420.0), + ..DialogSpec::FORM +}; + +pub(super) const USED_SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted.").into(), + body: |_, _, _| false, + confirm: confirm_used, + ok: Some("Expand Instances"), + discard: Some("Delete Instances"), + max_width: Some(420.0), + ..DialogSpec::FORM +}; + +/// Delete Symbol for symbol `name` of the active document. A symbol with instances always asks +/// what becomes of them; an unused one is deleted at once with `now` (Alt-click on the panel's +/// button) and asked about otherwise. +pub(crate) fn ask(app: &mut VectorcraftApp, name: &str, now: bool) { + let Some(uid) = app.session.active().map(|st| st.uid) else { return }; + match instances(app, uid, name) { + None => {} + Some(0) if now => { + if let Err(e) = app.run("symbol.delete", json!({ "name": name })) { + app.status(e); + } + } + Some(n) => open(app, if n == 0 { KIND } else { USED }, name, uid), + } +} + +fn open(app: &mut VectorcraftApp, kind: &str, name: &str, uid: u64) { + app.ui.dialog = Some(Dialog::new(kind, json!({ "name": name, "doc": uid }))); +} + +/// How many instances symbol `name` has, while the document `uid` is the active one and has the +/// symbol; `None` otherwise. +fn instances(app: &mut VectorcraftApp, uid: u64, name: &str) -> Option { + if app.session.active().map(|st| st.uid) != Some(uid) { + return None; + } + let list = app.run("symbol.list", json!({})).ok()?; + list["symbols"].as_array()?.iter().find(|s| s["name"].as_str() == Some(name)).map(|s| s["instances"].as_u64().unwrap_or(0)) +} + +/// The document and symbol `d` asks about. +fn target(d: &Dialog) -> (u64, String) { + (d.fields.get("doc").and_then(Value::as_u64).unwrap_or(0), d.str("name")) +} + +/// Yes: deletes the symbol, or asks the in-use question when it has gained instances. +fn confirm_unused(app: &mut VectorcraftApp, d: &Dialog) -> DialogResult { + app.ui.dialog = None; + let (uid, name) = target(d); + match instances(app, uid, &name) { + None => Ok(Value::Null), + Some(0) => app.run("symbol.delete", json!({ "name": name })), + Some(_) => { + open(app, USED, &name, uid); + Ok(Value::Null) + } + } +} + +/// Expand Instances (OK) turns the instances into ordinary art; Delete Instances (`discard`) +/// removes them. Either one deletes the symbol in one undo step. +fn confirm_used(app: &mut VectorcraftApp, d: &Dialog) -> DialogResult { + app.ui.dialog = None; + let (uid, name) = target(d); + if instances(app, uid, &name).is_none() { + return Ok(Value::Null); + } + app.run("symbol.delete", json!({ "name": name, "expandInstances": !d.bool("discard") })) +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 5b65923a1..2048bbcc9 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -735,6 +735,12 @@ font-technology Variable Променливи Delete Stop Изтриване на репер Delete Swatch Изтриване на образец Delete Symbol Изтриване на символ + Delete the selected symbol? Да се изтрие ли избраният символ? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Един или повече от символите се използват и не могат да бъдат изтрити, докато екземплярите им не бъдат разширени или изтрити. + Expand Instances Разширяване на екземплярите + Delete Instances Изтриване на екземплярите + Yes Да + No Не Delete Tab Изтриване на табулация Delete the graphic style “{name}”? Изтриване на графичния стил „{name}“? Delete Theme Изтриване на тема diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 5798a7a79..2ac957c9a 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -3046,6 +3046,12 @@ theme Appearance Izgled Current Symbol Trenutni simbol Delete Brush Izbriši četkicu Delete Symbol Izbriši simbol + Delete the selected symbol? Izbrisati odabrani simbol? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Jedan ili više simbola se koriste i ne mogu se izbrisati dok se njihove instance ne prošire ili izbrišu. + Expand Instances Proširi instance + Delete Instances Izbriši instance + Yes Da + No Ne Duplicate Brush Dupliciraj četkicu Duplicate Symbol Dupliciraj simbol Edit Symbol Uredi simbol diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index d93092418..ea8d62182 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1052,6 +1052,12 @@ Delete Styles Suprimeix els estils Delete Swatch Suprimeix la mostra Delete Symbol Suprimeix el símbol + Delete the selected symbol? Voleu suprimir el símbol seleccionat? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Un o més dels símbols estan en ús i no es poden suprimir fins que se n’expandeixin o se’n suprimeixin les instàncies. + Expand Instances Expandeix les instàncies + Delete Instances Suprimeix les instàncies + Yes Sí + No No Delete Tab Suprimeix la tabulació Delete the graphic style “{name}”? Voleu suprimir l’estil gràfic “{name}”? Delete the library “{name}”? Voleu suprimir la biblioteca “{name}”? diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 50a8f953e..93142cadb 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3078,6 +3078,12 @@ theme Appearance Motiv Current Symbol Aktuální symbol Delete Brush Odstranit štětec Delete Symbol Odstranit symbol + Delete the selected symbol? Odstranit vybraný symbol? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Jeden nebo více symbolů se používá a nelze je odstranit, dokud jejich instance nerozložíte nebo neodstraníte. + Expand Instances Rozložit instance + Delete Instances Odstranit instance + Yes Ano + No Ne Duplicate Brush Duplikovat štětec Duplicate Symbol Duplikovat symbol Edit Symbol Upravit symbol diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 87249badf..160abd6bf 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -2837,6 +2837,12 @@ theme Appearance Udseende Current Symbol Aktuelt symbol Delete Brush Slet pensel Delete Symbol Slet symbol + Delete the selected symbol? Vil du slette det markerede symbol? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Et eller flere af symbolerne er i brug og kan ikke slettes, før deres forekomster er udvidet eller slettet. + Expand Instances Udvid forekomster + Delete Instances Slet forekomster + Yes Ja + No Nej Duplicate Brush Duplikér pensel Duplicate Symbol Duplikér symbol Edit Symbol Redigér symbol diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 513ff8e4f..08869d765 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -729,6 +729,12 @@ font-technology Variable Variabel Delete Stop Farbstopp löschen Delete Swatch Farbfeld löschen Delete Symbol Symbol löschen + Delete the selected symbol? Ausgewähltes Symbol löschen? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Mindestens eines der Symbole wird verwendet und kann erst gelöscht werden, wenn seine Instanzen umgewandelt oder gelöscht wurden. + Expand Instances Instanzen umwandeln + Delete Instances Instanzen löschen + Yes Ja + No Nein Delete Tab Tabulator löschen Delete the graphic style “{name}”? Grafikstil „{name}“ löschen? Delete Theme Thema löschen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index ce282989f..3d074c716 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -709,6 +709,12 @@ font-technology Variable Μεταβλητές Delete Stop Διαγραφή στάσης ντεγκραντέ Delete Swatch Διαγραφή δείγματος χρώματος Delete Symbol Διαγραφή συμβόλου + Delete the selected symbol? Να διαγραφεί το επιλεγμένο σύμβολο; + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Ένα ή περισσότερα από τα σύμβολα χρησιμοποιούνται και δεν μπορούν να διαγραφούν μέχρι να επεκταθούν ή να διαγραφούν τα στιγμιότυπά τους. + Expand Instances Επέκταση στιγμιοτύπων + Delete Instances Διαγραφή στιγμιοτύπων + Yes Ναι + No Όχι Delete Tab Διαγραφή στηλοθέτη Delete the graphic style “{name}”? Να διαγραφεί το στυλ γραφικών «{name}»; Delete Theme Διαγραφή θέματος diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index fe945423e..c3b905d18 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -750,6 +750,12 @@ font-technology Variable Variables Delete Stop Eliminar parada Delete Swatch Eliminar muestra Delete Symbol Eliminar símbolo + Delete the selected symbol? ¿Eliminar el símbolo seleccionado? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Uno o más de los símbolos están en uso y no se pueden eliminar hasta que se expandan o eliminen sus instancias. + Expand Instances Expandir instancias + Delete Instances Eliminar instancias + Yes Sí + No No Delete Tab Eliminar tabulación Delete the graphic style “{name}”? ¿Eliminar el estilo gráfico “{name}”? Delete Theme Eliminar tema diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 7ebd23d6b..dd157932c 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -3018,6 +3018,12 @@ theme Appearance Ulkoasu Current Symbol Nykyinen symboli Delete Brush Poista sivellin Delete Symbol Poista symboli + Delete the selected symbol? Poistetaanko valittu symboli? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Yksi tai useampi symboleista on käytössä, eikä niitä voi poistaa ennen kuin niiden ilmentymät on laajennettu tai poistettu. + Expand Instances Laajenna ilmentymät + Delete Instances Poista ilmentymät + Yes Kyllä + No Ei Duplicate Brush Monista sivellin Duplicate Symbol Monista symboli Edit Symbol Muokkaa symbolia diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 859b2bfcd..1ff18594a 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -752,6 +752,12 @@ font-technology Variable Variables Delete Stop Supprimer l’étape de dégradé Delete Swatch Supprimer la nuance Delete Symbol Supprimer le symbole + Delete the selected symbol? Supprimer le symbole sélectionné ? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Un ou plusieurs symboles sont utilisés et ne peuvent pas être supprimés tant que leurs instances ne sont pas décomposées ou supprimées. + Expand Instances Décomposer les instances + Delete Instances Supprimer les instances + Yes Oui + No Non Delete Tab Supprimer la tabulation Delete the graphic style “{name}”? Supprimer le style graphique « {name} » ? Delete Theme Supprimer le thème diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 02e7303ed..254eb37fc 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -974,6 +974,12 @@ font-technology Variable Változó Delete Stop Színátmenet-pont törlése Delete Swatch Színminta törlése Delete Symbol Szimbólum törlése + Delete the selected symbol? Törli a kijelölt szimbólumot? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Egy vagy több szimbólum használatban van, és nem törölhető, amíg a példányait ki nem bontja vagy nem törli. + Expand Instances Példányok kibontása + Delete Instances Példányok törlése + Yes Igen + No Nem Delete Tab Tabulátor törlése Delete the graphic style “{name}”? Törlöd ezt a grafikai stílust: „{name}”? Delete the library “{name}”? Törlöd ezt a könyvtárat: „{name}”? diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index c2d369575..8e49c172b 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -2837,6 +2837,12 @@ theme Appearance Tampilan Current Symbol Simbol saat ini Delete Brush Hapus kuas Delete Symbol Hapus simbol + Delete the selected symbol? Hapus simbol yang dipilih? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Satu atau beberapa simbol sedang digunakan dan tidak dapat dihapus sampai instansinya diperluas atau dihapus. + Expand Instances Perluas instansi + Delete Instances Hapus instansi + Yes Ya + No Tidak Duplicate Brush Duplikatkan kuas Duplicate Symbol Duplikatkan simbol Edit Symbol Edit simbol diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 8021409d2..39addf361 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -750,6 +750,12 @@ font-technology Variable Variabili Delete Stop Elimina cursore colore Delete Swatch Elimina campione Delete Symbol Elimina simbolo + Delete the selected symbol? Eliminare il simbolo selezionato? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Uno o più simboli sono in uso e non possono essere eliminati finché le loro istanze non vengono espanse o eliminate. + Expand Instances Espandi istanze + Delete Instances Elimina istanze + Yes Sì + No No Delete Tab Elimina tabulazione Delete the graphic style “{name}”? Eliminare lo stile grafico “{name}”? Delete Theme Elimina tema diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 269651408..d9106487d 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1297,6 +1297,12 @@ font-technology Variable バリアブル Delete Stop 分岐点を削除 Delete Swatch スウォッチを削除 Delete Symbol シンボルを削除 + Delete the selected symbol? 選択したシンボルを削除しますか? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. 1 つ以上のシンボルが使用中のため、インスタンスを拡張または削除するまで削除できません。 + Expand Instances インスタンスを拡張 + Delete Instances インスタンスを削除 + Yes はい + No いいえ Delete Tab タブを削除 Delete Theme テーマを削除 Delete Transparency Flattener Preset 透明の分割・統合プリセットを削除 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index fd0eaae44..9c4d6ff2f 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -2650,6 +2650,12 @@ theme Appearance იერსახე Current Symbol მიმდინარე სიმბოლო Delete Brush ფუნჯის წაშლა Delete Symbol სიმბოლოს წაშლა + Delete the selected symbol? წაიშალოს მონიშნული სიმბოლო? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. ერთი ან მეტი სიმბოლო გამოიყენება და მისი წაშლა შეუძლებელია, სანამ მისი ეგზემპლარები არ გაიშლება ან არ წაიშლება. + Expand Instances ეგზემპლარების გაშლა + Delete Instances ეგზემპლარების წაშლა + Yes დიახ + No არა Duplicate Brush ფუნჯის დუბლირება Duplicate Symbol სიმბოლოს დუბლირება Edit Symbol სიმბოლოს რედაქტირება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 00666a840..f04039cdc 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1288,6 +1288,12 @@ font-technology Variable 가변 Delete Stop 정지점 삭제 Delete Swatch 견본 삭제 Delete Symbol 심볼 삭제 + Delete the selected symbol? 선택한 심볼을 삭제하시겠습니까? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. 하나 이상의 심볼이 사용 중이므로 인스턴스를 확장하거나 삭제하기 전에는 삭제할 수 없습니다. + Expand Instances 인스턴스 확장 + Delete Instances 인스턴스 삭제 + Yes 예 + No 아니요 Delete Tab 탭 삭제 Delete Theme 테마 삭제 Delete Transparency Flattener Preset 투명도 병합 프리셋 삭제 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index f04ea4da4..01d80c460 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2851,6 +2851,13 @@ theme Appearance Vormgeving Current Symbol Huidig symbool Delete Brush Penseel verwijderen Delete Symbol Symbool verwijderen + Delete the selected symbol? Het geselecteerde symbool verwijderen? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Een of meer van de symbolen zijn in gebruik en kunnen pas worden verwijderd als hun exemplaren zijn uitgebreid of verwijderd. + Expand Instances Exemplaren uitbreiden + Delete Instances Exemplaren verwijderen + Yes Ja + No Nee + Duplicate Brush Kwast dupliceren Duplicate Brush Penseel dupliceren Duplicate Symbol Symbool dupliceren Edit Symbol Symbool bewerken diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 17810fe50..84ac64f40 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2958,6 +2958,12 @@ theme Appearance Wygląd Current Symbol Bieżący symbol Delete Brush Usuń pędzel Delete Symbol Usuń symbol + Delete the selected symbol? Usunąć zaznaczony symbol? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Co najmniej jeden z symboli jest używany i nie można go usunąć, dopóki jego wystąpienia nie zostaną rozszerzone lub usunięte. + Expand Instances Rozszerz wystąpienia + Delete Instances Usuń wystąpienia + Yes Tak + No Nie Duplicate Brush Powiel pędzel Duplicate Symbol Powiel symbol Edit Symbol Edytuj symbol diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index e1fc3b3e6..e48ef54bd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -663,6 +663,12 @@ Delete Stop Excluir Parada Delete Swatch Excluir Amostra Delete Symbol Excluir Símbolo + Delete the selected symbol? Excluir o símbolo selecionado? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Um ou mais símbolos estão em uso e não podem ser excluídos até que suas instâncias sejam expandidas ou excluídas. + Expand Instances Expandir Instâncias + Delete Instances Excluir Instâncias + Yes Sim + No Não Delete Tab Excluir Parada de Tabulação Delete Theme Excluir Tema Delete Transparency Flattener Preset Excluir Predefinido de Achatamento de Transparência diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 412d4d129..c1567d10c 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -748,6 +748,12 @@ font-technology Variable Вариативные Delete Stop Удалить точку перехода Delete Swatch Удалить образец Delete Symbol Удалить символ + Delete the selected symbol? Удалить выбранный символ? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Один или несколько символов используются, и их нельзя удалить, пока их экземпляры не будут разобраны или удалены. + Expand Instances Разобрать экземпляры + Delete Instances Удалить экземпляры + Yes Да + No Нет Delete Tab Удалить табуляцию Delete the graphic style “{name}”? Удалить графический стиль «{name}»? Delete Theme Удалить тему diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 08b0b8105..c75c0f6ce 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -737,6 +737,12 @@ font-technology Variable Variabla Delete Stop Radera stopp Delete Swatch Radera färgruta Delete Symbol Radera symbol + Delete the selected symbol? Vill du radera den markerade symbolen? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. En eller flera av symbolerna används och kan inte raderas förrän deras instanser har expanderats eller raderats. + Expand Instances Expandera instanser + Delete Instances Radera instanser + Yes Ja + No Nej Delete Tab Radera tabb Delete the graphic style “{name}”? Radera grafikstilen ”{name}”? Delete Theme Radera tema diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index fff035657..21c15371a 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -729,6 +729,12 @@ font-technology Variable Değişken Delete Stop Durdurmayı Sil Delete Swatch Renk Örneğini Sil Delete Symbol Sembolü Sil + Delete the selected symbol? Seçili sembol silinsin mi? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Sembollerden biri veya birkaçı kullanımda ve örnekleri genişletilene ya da silinene kadar silinemez. + Expand Instances Örnekleri Genişlet + Delete Instances Örnekleri Sil + Yes Evet + No Hayır Delete Tab Sekmeyi Sil Delete the graphic style “{name}”? “{name}” grafik stili silinsin mi? Delete Theme Temayı Sil diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 1c6f93e5d..3117511dc 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -744,6 +744,12 @@ font-technology Variable Варіативні Delete Stop Видалити точку переходу Delete Swatch Видалити зразок Delete Symbol Видалити символ + Delete the selected symbol? Видалити вибраний символ? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Один або кілька символів використовуються, і їх не можна видалити, доки їхні екземпляри не буде розібрано або видалено. + Expand Instances Розібрати екземпляри + Delete Instances Видалити екземпляри + Yes Так + No Ні Delete Tab Видалити табуляцію Delete the graphic style “{name}”? Видалити графічний стиль «{name}»? Delete Theme Видалити тему diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 99d8437f8..b6d5604b3 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -870,6 +870,12 @@ font-technology Variable Biến thiên Delete Stop Xóa điểm dừng Delete Swatch Xóa mẫu màu Delete Symbol Xóa ký hiệu + Delete the selected symbol? Xóa ký hiệu đã chọn? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. Một hoặc nhiều ký hiệu đang được sử dụng và không thể xóa cho đến khi các thực thể của chúng được mở rộng hoặc xóa. + Expand Instances Mở rộng thực thể + Delete Instances Xóa thực thể + Yes Có + No Không Delete Tab Xóa tab Delete the graphic style “{name}”? Xóa kiểu đồ họa “{name}”? Delete the library “{name}”? Xóa thư viện “{name}”? diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 6ecaf0930..7c50ba577 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -693,6 +693,12 @@ Delete Stop 删除色标 Delete Swatch 删除色板 Delete Symbol 删除符号 + Delete the selected symbol? 要删除所选符号吗? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. 一个或多个符号正在使用中,在扩展或删除其实例之前无法删除。 + Expand Instances 扩展实例 + Delete Instances 删除实例 + Yes 是 + No 否 Delete Tab 删除制表位 Delete the graphic style “{name}”? 要删除图形样式“{name}”吗? Delete Theme 删除主题 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 75ee3a413..2d61281dd 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -712,6 +712,12 @@ font-technology Variable 可變 Delete Stop 刪除色標 Delete Swatch 刪除色票 Delete Symbol 刪除符號 + Delete the selected symbol? 要刪除選取的符號嗎? + One or more of the symbols are in use and cannot be deleted until their instances are expanded or deleted. 一個或多個符號正在使用中,在展開或刪除其範例之前無法刪除。 + Expand Instances 展開範例 + Delete Instances 刪除範例 + Yes 是 + No 否 Delete Tab 刪除定位點 Delete the graphic style “{name}”? 要刪除繪圖樣式「{name}」嗎? Delete Theme 刪除主題 diff --git a/crates/ui-egui/src/panels/symbols.rs b/crates/ui-egui/src/panels/symbols.rs index e99a24bf6..bc77f8e4c 100644 --- a/crates/ui-egui/src/panels/symbols.rs +++ b/crates/ui-egui/src/panels/symbols.rs @@ -273,10 +273,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let cmd = if ui.input(|i| i.modifiers.alt) { "symbol.new" } else { "ui.symbolOptions" }; app.run(cmd, json!({})).ok(); } + // Alt deletes an unused symbol without asking; a symbol with instances still asks. if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Symbol"), false, sel.is_some(), 24.0).clicked() && let Some(n) = &sel { - app.run("symbol.delete", json!({"name": n})).ok(); + crate::dialogs::symbol_delete::ask(app, n, super::alt_held(ui)); } }); } @@ -286,11 +287,23 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty()); let inst = has_instances_selected(app); let name = json!({"name": sel}); - let items: [(&str, bool, &str, Value); 10] = [ + let items: [(&str, bool, &str, Value); 3] = [ (tl!("New Symbol…"), has_sel, "ui.symbolOptions", json!({})), (tl!("Redefine Symbol"), has_sel && sel.is_some(), "symbol.update", name.clone()), (tl!("Duplicate Symbol"), sel.is_some(), "symbol.duplicate", name.clone()), - (tl!("Delete Symbol"), sel.is_some(), "symbol.delete", name.clone()), + ]; + for (label, enabled, cmd, params) in items { + if menu_item(ui, label, enabled, false) { + app.run(cmd, params).ok(); + } + } + // Asks first, as the panel's button does; the menu item has no Alt shortcut. + if menu_item(ui, tl!("Delete Symbol"), sel.is_some(), false) + && let Some(n) = &sel + { + crate::dialogs::symbol_delete::ask(app, n, false); + } + let items: [(&str, bool, &str, Value); 6] = [ (tl!("Symbol Options…"), sel.is_some(), "ui.symbolOptions", json!({"edit": true, "name": sel})), (tl!("Edit Symbol"), inst, "symbol.edit", json!({})), (tl!("Place Symbol Instance"), sel.is_some(), "symbol.place", name.clone()), @@ -738,6 +751,195 @@ mod tests { assert!(orange(3, 3) && orange(36, 36), "{:?} {:?}", thumb.pixels[3 * 40 + 3], thumb.pixels[36 * 40 + 36]); } + /// A document with symbol "Leaf", whose instance stands where its rectangle was, and symbol + /// "Dot", which no instance uses; "Dot" is current. + fn leaf_and_dot() -> VectorcraftApp { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + for (name, x) in [("Leaf", 10), ("Dot", 150)] { + app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 30, "height": 30})).unwrap(); + app.run("symbol.new", json!({"name": name})).unwrap(); + } + // Dot's instance goes: no art uses Dot. + app.run("edit.clear", json!({})).unwrap(); + app.run("symbol.setCurrent", json!({"name": "Dot"})).unwrap(); + app + } + + /// The instances of symbol `name` in the active document, and the ordinary paths in it. + fn art(app: &VectorcraftApp, name: &str) -> (usize, usize) { + let (mut instances, mut paths) = (0, 0); + for l in &app.session.active().unwrap().doc.layers { + l.walk(&mut |n| match &n.kind { + NodeKind::SymbolInstance { symbol, .. } if symbol == name => instances += 1, + NodeKind::Path { .. } => paths += 1, + _ => {} + }); + } + (instances, paths) + } + + fn dialog_kind(app: &VectorcraftApp) -> Option<&str> { + app.ui.dialog.as_ref().map(|d| d.kind.as_str()) + } + + /// One frame of the panel with `modifiers` held, its output kept. + fn frame_held(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec, modifiers: egui::Modifiers) -> egui::FullOutput { + let screen_rect = Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 400.0))); + let events = std::iter::once(egui::Event::ModifiersChanged(modifiers)).chain(events).collect(); + let mut out = ctx.run_ui(egui::RawInput { screen_rect, events, ..Default::default() }, |ui| show(app, ui)); + out.textures_delta.clear(); + out + } + + /// The labels of the clickable accessibility nodes of `out`, with their centers. + fn clickable(out: &egui::FullOutput) -> Vec<(String, Pos2)> { + let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes; + nodes + .iter() + .filter(|(_, n)| n.supports_action(egui::accesskit::Action::Click)) + .filter_map(|(_, n)| { + let b = n.bounds()?; + Some((n.label()?.trim().to_string(), pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32))) + }) + .collect() + } + + /// Clicks the panel's Delete Symbol button with `modifiers` held. + fn click_delete(app: &mut VectorcraftApp, modifiers: egui::Modifiers) { + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + let out = frame_held(&ctx, app, vec![], modifiers); + let at = clickable(&out).into_iter().find(|(l, _)| l == "Delete Symbol").expect("a Delete Symbol button").1; + let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers }; + frame_held(&ctx, app, vec![egui::Event::PointerMoved(at), press(true), press(false)], modifiers); + } + + /// Clicks the panel menu's Delete Symbol item. + fn menu_delete(app: &mut VectorcraftApp) { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let labels = menu_frame(&ctx, app, vec![]); + let item = labels.iter().find(|(t, _)| t.trim() == "Delete Symbol").map(|(_, r)| r.center()).expect("Delete Symbol in the menu"); + menu_frame(&ctx, app, vec![egui::Event::PointerMoved(item), button(item, true)]); + menu_frame(&ctx, app, vec![button(item, false)]); + } + + /// Delete Symbol, from the panel's button or its menu, asks "Delete the selected symbol?" for a + /// symbol no instance uses, and the in-use question for one with instances, before changing + /// the document; No leaves it as it was and Yes deletes the symbol. Alt-click on the button + /// deletes an unused symbol without asking, and still asks for one with instances. + #[test] + fn delete_symbol_asks_first_and_alt_click_deletes_an_unused_symbol_at_once() { + let mut app = leaf_and_dot(); + let before = app.session.active().unwrap().doc.clone(); + let unchanged = |app: &VectorcraftApp| Arc::ptr_eq(&before, &app.session.active().unwrap().doc); + click_delete(&mut app, egui::Modifiers::NONE); + assert_eq!(dialog_kind(&app), Some(crate::dialogs::symbol_delete::KIND)); + assert!(unchanged(&app), "nothing changes while it asks"); + crate::dialogs::cancel(&mut app); + assert!(app.ui.dialog.is_none()); + assert!(unchanged(&app), "No changes nothing"); + menu_delete(&mut app); + assert_eq!(dialog_kind(&app), Some(crate::dialogs::symbol_delete::KIND), "the menu item asks too"); + assert!(unchanged(&app)); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(symbols(&mut app), ["Leaf"], "Yes deletes it"); + // Alt-click on the button: an unused symbol goes without a question. + app.run("edit.undo", json!({})).unwrap(); + app.run("symbol.setCurrent", json!({"name": "Dot"})).unwrap(); + click_delete(&mut app, egui::Modifiers::ALT); + assert!(app.ui.dialog.is_none()); + assert_eq!(symbols(&mut app), ["Leaf"]); + // A symbol with an instance asks, with Alt held too, from the button and from the menu. + app.run("symbol.setCurrent", json!({"name": "Leaf"})).unwrap(); + let before = app.session.active().unwrap().doc.clone(); + for alt in [egui::Modifiers::NONE, egui::Modifiers::ALT] { + click_delete(&mut app, alt); + assert_eq!(dialog_kind(&app), Some(crate::dialogs::symbol_delete::USED), "{alt:?}"); + crate::dialogs::cancel(&mut app); + } + menu_delete(&mut app); + assert_eq!(dialog_kind(&app), Some(crate::dialogs::symbol_delete::USED)); + crate::dialogs::cancel(&mut app); + assert!(Arc::ptr_eq(&before, &app.session.active().unwrap().doc), "Cancel changes nothing"); + assert_eq!(art(&app, "Leaf"), (1, 0)); + } + + /// The in-use question: Expand Instances deletes the symbol and turns its instance into + /// ordinary art, Delete Instances deletes the symbol and its instance, each in one undo step. + #[test] + fn the_in_use_question_expands_or_deletes_the_instances() { + for (discard, left) in [(false, (0, 1)), (true, (0, 0))] { + let mut app = leaf_and_dot(); + crate::dialogs::symbol_delete::ask(&mut app, "Leaf", false); + assert_eq!(dialog_kind(&app), Some(crate::dialogs::symbol_delete::USED)); + if discard { + app.ui.dialog.as_mut().unwrap().fields.insert("discard".into(), json!(true)); + } + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(symbols(&mut app), ["Dot"], "discard {discard}"); + assert_eq!(art(&app, "Leaf"), left, "discard {discard}"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!((symbols(&mut app), art(&app, "Leaf")), (vec!["Leaf".to_string(), "Dot".to_string()], (1, 0)), "one undo step"); + } + } + + /// A question asked in one document deletes nothing once another is active, and Yes for a + /// symbol that has gained an instance since asks the in-use question instead. + #[test] + fn a_stale_delete_symbol_question_deletes_nothing() { + let mut app = leaf_and_dot(); + crate::dialogs::symbol_delete::ask(&mut app, "Dot", false); + let asked = app.ui.dialog.clone(); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 30})).unwrap(); + app.run("symbol.new", json!({"name": "Dot"})).unwrap(); + app.run("edit.clear", json!({})).unwrap(); + app.ui.dialog = asked; + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert_eq!(symbols(&mut app), ["Dot"], "the other document keeps its symbol"); + // Back in the first document: Dot gains an instance while the question is open. + app.run("document.activate", json!({"index": 0})).unwrap(); + crate::dialogs::symbol_delete::ask(&mut app, "Dot", false); + assert_eq!(dialog_kind(&app), Some(crate::dialogs::symbol_delete::KIND)); + app.run("symbol.place", json!({"name": "Dot"})).unwrap(); + crate::dialogs::confirm(&mut app).unwrap(); + assert_eq!(dialog_kind(&app), Some(crate::dialogs::symbol_delete::USED)); + assert_eq!(symbols(&mut app), ["Leaf", "Dot"]); + assert_eq!(art(&app, "Dot"), (1, 0)); + } + + /// The questions' buttons: Yes and No for an unused symbol; Expand Instances, Delete Instances + /// and Cancel for one with instances. + #[test] + fn the_delete_symbol_questions_show_their_buttons() { + let mut app = leaf_and_dot(); + for (name, want) in [("Dot", &["Yes", "No"][..]), ("Leaf", &["Expand Instances", "Delete Instances", "Cancel"][..])] { + crate::dialogs::symbol_delete::ask(&mut app, name, false); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + // The window measures itself in its first frame and draws its buttons in the next. + let mut out = None; + for _ in 0..2 { + let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), ..Default::default() }; + let mut o = ctx.run_ui(raw, |ui| crate::dialogs::show(&mut app, ui.ctx())); + o.textures_delta.clear(); + out = Some(o); + } + let out = out.expect("a frame"); + let labels: Vec = clickable(&out).into_iter().map(|(l, _)| l).collect(); + for w in want { + assert!(labels.iter().any(|l| l == w), "{name}: {w} in {labels:?}"); + } + assert!(!labels.iter().any(|l| l == "OK"), "{name}: {labels:?}"); + crate::dialogs::cancel(&mut app); + } + } + /// #4121: with symbol instances selected, the Control bar's Replace menu lists the document's /// symbols and shows the instance's own; picking another replaces it. #[test] diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 97e9abf9f..6d97e0097 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -597,6 +597,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - macOS: Finder opens every format File › Open reads with VectorCraft (double-click, Open With, a drop on the Dock icon, `open -a`), at launch or while it runs; the release and `cargo xtask bundle` apps share one `Info.plist` that declares those document types, owning only VectorCraft's own formats. The DMG opens on a designed Finder window, VectorCraft.app beside the `Applications` link over a background with the app icon (#493). - Swatch and graphic style libraries, Libraries panel files, presets and plug-ins open wherever they are dropped on the window, as File › Open opens them. - **Symbol libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html)): Window › Symbol Libraries and the Symbols panel's Symbol Libraries Menu open each library in a library panel of its own, as swatch and graphic style libraries do. The built-in library, Basic Shapes, is original art generated in code (`crates/engine/src/cmd/symbollib.rs`). A click on a symbol adds it to the document's Symbols as one undo step, with its Symbol Options (size, registration, export type, 9-slice guides) and every resource it uses (the symbols it nests, patterns, swatches, brushes, graphic styles, images), and makes it the current symbol; a symbol the document already has is not added again and another symbol of the same name keeps its name, the library's comes in numbered. Thumbnails, in the library panels and the Symbols panel, draw a symbol with the symbols it nests and the patterns and images it uses. Dragging a library symbol onto the canvas also places an instance there, in the same step ([placing from a library](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-and-place-symbols.html)). Save Symbols (the library menu) and Save Symbol Library… (the Symbols panel menu) write the document's symbols as a `.vectorcraft` document holding only them, into the user library folder (listed under User Defined) or a file; Other Library… reads the symbols of any document VectorCraft opens; a file it can't read is an error and opens no panel. Tests: `crates/engine/src/tests/symbollib.rs`, `crates/ui-egui/src/panels/tests_symbol_libraries.rs`, `panels::symbols::tests::thumbnails_show_the_symbols_a_symbol_nests`, `canvas::tests::a_library_symbol_dropped_on_the_canvas_is_added_and_placed_there_in_one_step`. + - **Delete Symbol asks first** ([Symbols panel](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/symbols-panel-options.html), [symbol libraries](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/create-or-import-symbol-libraries.html), [Alt-click Delete](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)): the Symbols panel's Delete Symbol button and menu item ask "Delete the selected symbol?" with Yes and No for a symbol no instance uses. For a symbol with placed instances they ask with Expand Instances (the instances become ordinary art), Delete Instances and Cancel; either Instances button deletes the symbol in one undo step. Alt-click on the button deletes an unused symbol without asking and still asks about one with instances. A question left open while another document became active deletes nothing. `symbol.delete` itself does not ask. Tests: `panels::symbols::tests::delete_symbol_asks_first_and_alt_click_deletes_an_unused_symbol_at_once`, `the_in_use_question_expands_or_deletes_the_instances`, `a_stale_delete_symbol_question_deletes_nothing`, `the_delete_symbol_questions_show_their_buttons`. - **Brush libraries** ([public source](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/create-new-brush-libraries.html), [import](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/import-brushes-from-brush-libraries-or-other-files.html), [Brushes panel](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/apply-and-edit-strokes/brushes-panel-overview.html)): Window › Brush Libraries, the Brushes panel's Brush Libraries Menu and its menu's Open Brush Library open each library in a library panel of its own, as swatch, graphic style and symbol libraries do. The built-in libraries (Art Ribbons, Calligraphic Nibs, Pattern Borders, Scatter Shapes) are original brushes generated in code (`crates/engine/src/cmd/brushlib.rs`); Striped Ribbon's art is filled with the library's own pattern and Sparkles scatters the library's own symbol. A click on a brush adds it to the document's Brushes as one undo step with every resource its art uses (patterns, symbols, swatches, graphic styles, images), makes it the current brush and, with paths selected, applies it to them in the same step; a brush the document already has is not added again and a different brush of the same name comes in numbered ("Wave 2"). Shift/Cmd-clicks select brushes for Add to Brushes; a brush dragged onto a path is added and applied to it, and one dragged onto the Brushes panel is added. Save Brushes… / the Brushes panel's Save Brush Library… write the document's brushes (or the selected ones) as a `.vectorcraft` library into the `Brushes` folder next to the preferences, listed under User Defined, or to a file; Other Library… reads the brushes of any document VectorCraft opens. Thumbnails, in the library panels, the Brushes panel and Brush Options, render brush art with the patterns and symbols it uses. Copying brushed art now carries the patterns and symbols its brush's art uses too. Tests: `crates/engine/src/tests/brushlib.rs`, `crates/ui-egui/src/panels/tests_brush_libraries.rs`, `panels::brushes::tests::library_brush_previews_show_the_patterns_their_art_uses`, `canvas::tests::a_library_brush_dropped_on_a_path_is_added_and_applied_in_one_step`. - A canvas context menu: right-click selects the object under the pointer and lists what applies to the selection (Undo/Redo, clipboard, group, isolation, join, Remove Anchor Points when anchors are selected, masks, compound paths, guides, Transform, Arrange, Select, Export Selection), or the view commands on empty canvas. - A right-click over locked objects adds an Unlock submenu to the canvas context menu. The submenu lists up to 20 locked objects under the pointer, topmost first, by the name the Layers panel shows, and choosing one unlocks it (`layer.setProps`). A locked group is listed as one item. Objects on hidden, locked or template layers are not listed.