diff --git a/crates/doc/src/variables.rs b/crates/doc/src/variables.rs index 360de81c9..96bccbd0a 100644 --- a/crates/doc/src/variables.rs +++ b/crates/doc/src/variables.rs @@ -116,6 +116,10 @@ pub struct Variables { pub bindings: BTreeMap>, #[serde(default, skip_serializing_if = "Option::is_none")] pub active_dataset: Option, + /// Lock Variables: while set, variables can't be defined, deleted or renamed. Objects can + /// still be bound to them and unbound, and datasets still change and apply. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub locked: bool, } impl Variables { diff --git a/crates/engine/src/cmd/variablecmds.rs b/crates/engine/src/cmd/variablecmds.rs index 3694f8e54..fb82f1f4b 100644 --- a/crates/engine/src/cmd/variablecmds.rs +++ b/crates/engine/src/cmd/variablecmds.rs @@ -29,6 +29,19 @@ const MAX_CSV_BYTES: usize = 1_048_576; const MAX_CSV_ROWS: usize = 1_000; const MAX_CSV_COLS: usize = 256; +/// Lock Variables refuses New Variable, Delete Variable, Rename Variable (Variable Options) and +/// Make … Dynamic; binding, unbinding and the dataset commands still run. +fn variables_unlocked(s: &Session) -> std::result::Result<(), String> { + has_doc(s)?; + if s.active().is_some_and(|st| st.doc.variables.locked) { Err("variables are locked".into()) } else { Ok(()) } +} + +/// Make … Dynamic needs a selection, and defines a variable, which Lock Variables refuses. +fn selection_and_unlocked(s: &Session) -> std::result::Result<(), String> { + has_selection(s)?; + variables_unlocked(s) +} + fn var_name(p: &Value, cmd: &str) -> Result { named(str_param(p, "name"), "name", cmd) } @@ -232,7 +245,21 @@ fn list(s: &mut Session, _p: &Value) -> Result { json!({"name": v.name, "kind": v.kind.label(), "bindings": bindings}) }) .collect(); - Ok(json!({"variables": variables})) + Ok(json!({"variables": variables, "locked": st.doc.variables.locked})) +} + +/// Lock Variables: without `locked` it toggles. Setting the state the document already has +/// does not edit the document or add an undo step. +fn lock(s: &mut Session, p: &Value) -> Result { + let was = s.doc()?.doc.variables.locked; + let locked = bool_or(p, "locked", !was); + if locked != was { + s.edit(if locked { "Lock Variables" } else { "Unlock Variables" }, |d, _| { + d.variables.locked = locked; + Ok(()) + })?; + } + Ok(json!({"locked": locked})) } fn bind(s: &mut Session, p: &Value) -> Result { @@ -812,8 +839,8 @@ pub fn specs() -> Vec { "New Variable…", ["Window", "Variables"], None, - "{name, kind: text|visibility|image|graph} define a variable (graph: CSV data on a graph root)", - has_doc, + "{name, kind: text|visibility|image|graph} define a variable (graph: CSV data on a graph root); refused while the variables are locked", + variables_unlocked, define ), cmd!( @@ -821,8 +848,8 @@ pub fn specs() -> Vec { "Delete Variable", ["Window", "Variables"], None, - "{names?} delete variables with their bindings and values (default: the highlighted one)", - has_doc, + "{names?} delete variables with their bindings and values (default: the highlighted one); refused while the variables are locked", + variables_unlocked, delete ), cmd!( @@ -830,18 +857,18 @@ pub fn specs() -> Vec { "Rename Variable", ["Window", "Variables"], None, - "{name, newName} rename a variable, with its bindings and its values in every dataset", - has_doc, + "{name, newName} rename a variable, with its bindings and its values in every dataset; refused while the variables are locked", + variables_unlocked, rename ), - cmd!(query "variable.list", "List Variables", [], None, "{} → variables with kinds and binding counts", has_doc, list), + cmd!(query "variable.list", "List Variables", [], None, "{} → variables with kinds and binding counts, and whether they are locked → {variables, locked}", has_doc, list), cmd!( "variable.makeTextDynamic", "Make Text Dynamic", ["Window", "Variables"], None, - "{} define a text variable over the selection, named after it and bound to it", - has_selection, + "{} define a text variable over the selection, named after it and bound to it; refused while the variables are locked", + selection_and_unlocked, |s, _| make_dynamic(s, VariableKind::Text, "Text") ), cmd!( @@ -849,8 +876,8 @@ pub fn specs() -> Vec { "Make Visibility Dynamic", ["Window", "Variables"], None, - "{} define a visibility variable over the selection, named after it and bound to it", - has_selection, + "{} define a visibility variable over the selection, named after it and bound to it; refused while the variables are locked", + selection_and_unlocked, |s, _| make_dynamic(s, VariableKind::Visibility, "Visibility") ), cmd!( @@ -858,8 +885,8 @@ pub fn specs() -> Vec { "Make Image Dynamic", ["Window", "Variables"], None, - "{} define an image variable over the selection, named after it and bound to it", - has_selection, + "{} define an image variable over the selection, named after it and bound to it; refused while the variables are locked", + selection_and_unlocked, |s, _| make_dynamic(s, VariableKind::Image, "Image") ), cmd!( @@ -867,8 +894,8 @@ pub fn specs() -> Vec { "Make Graph Dynamic", ["Window", "Variables"], None, - "{} define a graph variable over the selected graph root(s), named after it and bound to it; each dataset row supplies CSV as graph.setData", - has_selection, + "{} define a graph variable over the selected graph root(s), named after it and bound to it; each dataset row supplies CSV as graph.setData; refused while the variables are locked", + selection_and_unlocked, |s, _| make_dynamic(s, VariableKind::Graph, "Graph") ), cmd!( @@ -889,6 +916,15 @@ pub fn specs() -> Vec { has_doc, unbind ), + cmd!( + "variable.lock", + "Lock Variables", + ["Window", "Variables"], + None, + "{locked?: bool} lock or unlock the variables (default: toggle); saved with the document; a change is one undo step. While locked, variable.define, variable.delete, variable.rename and variable.make…Dynamic are refused; binding, unbinding and the dataset commands still run → {locked}", + has_doc, + lock + ), cmd!( "variable.highlight", "Highlight Variable", diff --git a/crates/engine/src/tests_variables.rs b/crates/engine/src/tests_variables.rs index 62bfa9e40..b1d8d28d2 100644 --- a/crates/engine/src/tests_variables.rs +++ b/crates/engine/src/tests_variables.rs @@ -370,6 +370,79 @@ fn make_dynamic_errors_name_the_command() { assert!(e.contains("`variable.makeTextDynamic`") && e.contains("is not type"), "{e}"); } +/// Lock Variables refuses defining, deleting and renaming variables, Make … Dynamic included, +/// while binding, unbinding and the dataset commands still run. +#[test] +fn locked_variables_refuse_new_deleted_and_renamed_variables() { + let mut s = session(); + let t = text(&mut s, "Alice"); + let r = rect(&mut s); + s.execute("variable.define", &json!({"name": "Name", "kind": "text"})).unwrap(); + s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap(); + assert_eq!(s.execute("variable.list", &json!({})).unwrap()["locked"], false); + assert_eq!(s.execute("variable.lock", &json!({})).unwrap()["locked"], true, "without `locked` it toggles"); + assert_eq!(s.execute("variable.list", &json!({})).unwrap()["locked"], true); + + s.execute("select.set", &json!({"ids": [t.0]})).unwrap(); + for (cmd, p) in [ + ("variable.define", json!({"name": "Other", "kind": "text"})), + ("variable.delete", json!({"names": ["Name"]})), + ("variable.rename", json!({"name": "Name", "newName": "Renamed"})), + ("variable.makeTextDynamic", json!({})), + ("variable.makeVisibilityDynamic", json!({})), + ] { + let e = s.execute(cmd, &p).unwrap_err(); + assert!(matches!(&e, EngineError::Disabled(_, why) if why == "variables are locked"), "{cmd}: {e}"); + } + let names = |s: &mut Session| { + s.execute("variable.list", &json!({})).unwrap()["variables"].as_array().unwrap().iter().map(|v| v["name"].clone()).collect::>() + }; + assert_eq!(names(&mut s), [json!("Name"), json!("Show")], "the variables are as they were"); + + // Binding and unbinding still work, and so do the datasets. + s.execute("variable.bind", &json!({"variable": "Name", "ids": [t.0]})).unwrap(); + s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap(); + s.execute("variable.unbind", &json!({"ids": [r.0], "variable": "Show"})).unwrap(); + s.execute("dataset.capture", &json!({})).unwrap(); + s.execute("dataset.new", &json!({"name": "Bob", "values": {"Name": "Bob"}})).unwrap(); + s.execute("dataset.import", &json!({"csv": "name,Name\nCarol,Carol\n"})).unwrap(); + s.execute("dataset.select", &json!({"name": "Carol"})).unwrap(); + assert_eq!(plain(&s, t), "Carol"); + + // Unlocking lets a variable be defined again; `locked` sets the state rather than toggling. + assert_eq!(s.execute("variable.lock", &json!({"locked": false})).unwrap()["locked"], false); + assert_eq!(s.execute("variable.lock", &json!({"locked": false})).unwrap()["locked"], false); + s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap(); +} + +/// Locking is an undo step only when it changes the state, and the lock is saved with the +/// document: a file saved locked opens locked, and a file without the key opens unlocked. +#[test] +fn the_variables_lock_is_an_undo_step_and_is_saved() { + let mut s = session(); + let steps = |s: &Session| s.doc().unwrap().history.undo.len(); + let before = steps(&s); + s.execute("variable.lock", &json!({"locked": true})).unwrap(); + assert_eq!(steps(&s), before + 1); + s.execute("variable.lock", &json!({"locked": true})).unwrap(); + assert_eq!(steps(&s), before + 1, "locking locked variables is not a step"); + assert!(s.doc().unwrap().is_dirty()); + + let saved = vectorcraft_format::save(&s.doc().unwrap().doc, false); + assert!(vectorcraft_format::load(&saved).unwrap().variables.locked, "a document saved locked opens locked"); + let mut v: Value = serde_json::from_slice(&saved).unwrap(); + assert_eq!(v["document"]["variables"]["locked"], true); + v["document"]["variables"].as_object_mut().unwrap().remove("locked"); + assert!(!vectorcraft_format::load(&serde_json::to_vec(&v).unwrap()).unwrap().variables.locked, "a file without the key opens unlocked"); + + s.execute("edit.undo", &json!({})).unwrap(); + assert!(!s.doc().unwrap().doc.variables.locked, "undo unlocks"); + let v: Value = serde_json::from_slice(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap(); + assert!(v["document"].get("variables").is_none_or(|v| v.get("locked").is_none()), "unlocked variables write no key: {v}"); + s.execute("edit.redo", &json!({})).unwrap(); + assert!(s.doc().unwrap().doc.variables.locked, "redo locks again"); +} + /// Capture Data Set records what the art holds now, which is how rows are made — the art is /// edited and captured, rather than the values typed. #[test] diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 567263cfc..2c520f028 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3266,6 +3266,8 @@ Variables Променливи New Variable Нова променлива Delete Variable Изтриване на променлива + Lock Variables Заключване на променливите + Unlock Variables Отключване на променливите List Variables Списък с променливи Bind Variable Свързване на променлива Unbind Variable Отвързване на променлива @@ -4046,6 +4048,7 @@ axis Both И двете @msg unknown tool or command `{key}` непознат инструмент или команда `{key}` @msg unknown workspace (one of {_1}) непознато работно пространство (едно от {_1}) @msg unsupported: {0} не се поддържа: {0} +@msg variables are locked променливите са заключени @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files файловете от версия {0} не могат да бъдат компресирани: само версия {COMPRESSED_SINCE} и по-нови отварят компресирани файлове @msg width and height are at least 1 pixel ширината и височината са най-малко 1 пиксел @msg workspace name can't be empty името на работното пространство не може да бъде празно diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 73416d3c5..a161b8e64 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -653,6 +653,8 @@ New Variable… Nová proměnná… Unbind Variable Zrušit přiřazení proměnné Delete Variable Odstranit proměnnou + Lock Variables Zamknout proměnné + Unlock Variables Odemknout proměnné New Data Set… Nová sada dat… Edit Data Set… Upravit sadu dat… Delete Data Set Odstranit sadu dat @@ -4420,6 +4422,7 @@ imageTrace Outline Obrys @msg unknown tool or command `{key}` neznámý nástroj nebo příkaz `{key}` @msg unknown workspace (one of {_1}) neznámý pracovní prostor (jeden z {_1}) @msg unsupported: {0} nepodporováno: {0} +@msg variables are locked proměnné jsou zamčené @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files soubory verze {0} nelze komprimovat: komprimované soubory otevírá až verze {COMPRESSED_SINCE} a novější @msg width and height are at least 1 pixel šířka i výška jsou alespoň 1 obrazový bod @msg workspace name can't be empty název pracovního prostoru nesmí být prázdný diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 802e182d1..0b609d158 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4047,6 +4047,7 @@ theme Appearance Erscheinungsbild @msg unknown tool or command `{key}` unbekanntes Werkzeug oder unbekannter Befehl `{key}` @msg unknown workspace (one of {_1}) unbekannter Arbeitsbereich (einer von {_1}) @msg unsupported: {0} nicht unterstützt: {0} +@msg variables are locked die Variablen sind gesperrt @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files Dateien der Version {0} können nicht komprimiert werden: Erst Version {COMPRESSED_SINCE} und neuer öffnen komprimierte Dateien @msg width and height are at least 1 pixel Breite und Höhe betragen mindestens 1 Pixel @msg workspace name can't be empty der Name des Arbeitsbereichs darf nicht leer sein @@ -4313,6 +4314,8 @@ theme Appearance Erscheinungsbild Highlight Variable Variable hervorheben New Variable Neue Variable Delete Variable Variable löschen + Lock Variables Variablen sperren + Unlock Variables Variablen entsperren List Variables Variablen auflisten Bind Variable Variable binden Unbind Variable Variable lösen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index b6528bebb..6c4e23cc1 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3580,6 +3580,8 @@ theme Appearance Εμφάνιση Variable Width Profile Προφίλ μεταβλητού πλάτους New Variable Νέα μεταβλητή Delete Variable Διαγραφή μεταβλητής + Lock Variables Κλείδωμα μεταβλητών + Unlock Variables Ξεκλείδωμα μεταβλητών List Variables Λίστα μεταβλητών Bind Variable Σύνδεση μεταβλητής Unbind Variable Αποσύνδεση μεταβλητής @@ -4097,6 +4099,7 @@ theme Appearance Εμφάνιση @msg unknown tool or command `{key}` άγνωστο εργαλείο ή εντολή `{key}` @msg unknown workspace (one of {_1}) άγνωστος χώρος εργασίας (ένας από τους {_1}) @msg unsupported: {0} δεν υποστηρίζεται: {0} +@msg variables are locked οι μεταβλητές είναι κλειδωμένες @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files τα αρχεία έκδοσης {0} δεν μπορούν να συμπιεστούν: μόνο η έκδοση {COMPRESSED_SINCE} και νεότερες ανοίγουν συμπιεσμένα αρχεία @msg width and height are at least 1 pixel το πλάτος και το ύψος πρέπει να είναι τουλάχιστον 1 εικονοστοιχείο @msg workspace name can't be empty το όνομα χώρου εργασίας δεν μπορεί να είναι κενό diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 61be275b3..1f3a92f02 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3325,6 +3325,8 @@ Variables Variables New Variable Nueva variable Delete Variable Eliminar variable + Lock Variables Bloquear variables + Unlock Variables Desbloquear variables List Variables Enumerar variables Bind Variable Vincular variable Unbind Variable Desvincular variable @@ -4121,6 +4123,7 @@ axis Both Ambos @msg unknown tool or command `{key}` herramienta o comando desconocido `{key}` @msg unknown workspace (one of {_1}) espacio de trabajo desconocido (uno de estos: {_1}) @msg unsupported: {0} no compatible: {0} +@msg variables are locked las variables están bloqueadas @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files los archivos de la versión {0} no se pueden comprimir: solo la versión {COMPRESSED_SINCE} y posteriores abren archivos comprimidos @msg width and height are at least 1 pixel el ancho y el alto son de al menos 1 píxel @msg workspace name can't be empty el nombre del espacio de trabajo no puede estar vacío diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index a3a6e7902..c7031c834 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -644,6 +644,8 @@ New Variable… Uusi muuttuja… Unbind Variable Pura muuttujan sidonta Delete Variable Poista muuttuja + Lock Variables Lukitse muuttujat + Unlock Variables Avaa muuttujien lukitus New Data Set… Uusi tietojoukko… Edit Data Set… Muokkaa tietojoukkoa… Delete Data Set Poista tietojoukko @@ -4342,6 +4344,7 @@ imageTrace Outline Ääriviiva @msg unknown tool or command `{key}` tuntematon työkalu tai komento `{key}` @msg unknown workspace (one of {_1}) tuntematon työtila (jokin seuraavista: {_1}) @msg unsupported: {0} ei tuettu: {0} +@msg variables are locked muuttujat on lukittu @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files version {0} tiedostoja ei voi pakata: pakattuja tiedostoja voi avata vain versiosta {COMPRESSED_SINCE} alkaen @msg width and height are at least 1 pixel leveys ja korkeus ovat vähintään 1 pikseli @msg workspace name can't be empty työtilan nimi ei voi olla tyhjä diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index bf3205141..af97d9c54 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3322,6 +3322,8 @@ Variables Variables New Variable Nouvelle variable Delete Variable Supprimer la variable + Lock Variables Verrouiller les variables + Unlock Variables Déverrouiller les variables List Variables Lister les variables Bind Variable Lier la variable Unbind Variable Délier la variable @@ -4118,6 +4120,7 @@ axis Both Les deux @msg unknown tool or command `{key}` outil ou commande inconnu `{key}` @msg unknown workspace (one of {_1}) espace de travail inconnu (valeurs possibles : {_1}) @msg unsupported: {0} non pris en charge : {0} +@msg variables are locked les variables sont verrouillées @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files les fichiers de version {0} ne peuvent pas être compressés : seules les versions {COMPRESSED_SINCE} et ultérieures ouvrent les fichiers compressés @msg width and height are at least 1 pixel la largeur et la hauteur valent au moins 1 pixel @msg workspace name can't be empty le nom de l’espace de travail ne peut pas être vide diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index efabf2eef..6a829c0c2 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2093,6 +2093,7 @@ file-filter Templates Sablonok Lock Others Többi zárolása Lock Slices Szeletek zárolása Lock Station Point Nézőpont zárolása + Lock Variables Változók zárolása Logo 1000×1000 Logó 1000×1000 Long Lines Hosszú vonalak Long Strokes Hosszú vonások @@ -3956,6 +3957,7 @@ mark-style Japanese Japán Unlock All Layers Összes réteg zárolásának feloldása Unlock Grid Rács zárolásának feloldása Unlock Guides Segédvonalak zárolásának feloldása + Unlock Variables Változók zárolásának feloldása Unnamed Objects Névtelen objektumok Unprintable margin: Nem nyomtatható margó: Unsharp Mask Életlenítő maszk @@ -4841,6 +4843,7 @@ imageTrace Outline Körvonal @msg unsupported: {0} nem támogatott: {0} @msg Updated {_1} image(s) Frissített képek: {_1} @msg Updated {_1} linked file(s) changed on disk Frissített, a lemezen módosult csatolt fájlok: {_1} +@msg variables are locked a változók zárolva vannak @msg VectorCraft doesn't search other apps' or the system's folders: {path} a VectorCraft nem keres más alkalmazások és a rendszer mappáiban: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} a VectorCraft nem keres a Users alatti Shared és Public mappában: {path} @msg VectorCraft failed to start: {e} A VectorCraft nem tudott elindulni: {e} diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 87f94d523..b387698f0 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3345,6 +3345,8 @@ mark-style Japanese Giapponese Variables Variabili New Variable Nuova variabile Delete Variable Elimina variabile + Lock Variables Blocca variabili + Unlock Variables Sblocca variabili List Variables Elenca variabili Bind Variable Collega variabile Unbind Variable Scollega variabile @@ -4146,6 +4148,7 @@ axis Both Entrambi @msg unknown tool or command `{key}` strumento o comando sconosciuto `{key}` @msg unknown workspace (one of {_1}) area di lavoro sconosciuta (una delle seguenti: {_1}) @msg unsupported: {0} non supportato: {0} +@msg variables are locked le variabili sono bloccate @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files i file della versione {0} non possono essere compressi: solo la versione {COMPRESSED_SINCE} e successive aprono file compressi @msg width and height are at least 1 pixel larghezza e altezza sono di almeno 1 pixel @msg workspace name can't be empty il nome dell'area di lavoro non può essere vuoto diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index f903169bd..f99af9c01 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -615,6 +615,8 @@ Variables 変数 New Variable 新規変数 Delete Variable 変数を削除 + Lock Variables 変数をロック + Unlock Variables 変数をロック解除 List Variables 変数一覧 Bind Variable 変数をバインド Unbind Variable 変数のバインドを解除 @@ -3769,6 +3771,7 @@ swatch Bright Violet ビビッドバイオレット @msg {command} didn't create an object {command} でオブジェクトが作成されませんでした @msg Make Guides: select paths ガイドを作成: パスを選択してください @msg Release Guides: no guides ガイドを解除: ガイドがありません +@msg variables are locked 変数がロックされています @msg guides are locked ガイドがロックされています @msg handle must be in or out handle には in または out を指定してください @msg no saved selection recalled 保存した選択範囲を呼び出していません diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index d0d967763..93dd5e233 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -613,6 +613,8 @@ Variables 변수 New Variable 새 변수 Delete Variable 변수 삭제 + Lock Variables 변수 잠그기 + Unlock Variables 변수 잠금 해제 List Variables 변수 목록 Bind Variable 변수 연결 Unbind Variable 변수 연결 해제 @@ -3701,6 +3703,7 @@ theme Appearance 외관 @msg {command} didn't create an object {command}이(가) 개체를 만들지 않았습니다 @msg Make Guides: select paths 안내선 만들기: 패스를 선택하세요 @msg Release Guides: no guides 안내선 해제: 안내선이 없습니다 +@msg variables are locked 변수가 잠겨 있습니다 @msg guides are locked 안내선이 잠겨 있습니다 @msg handle must be in or out handle은 in 또는 out이어야 합니다 @msg no saved selection recalled 불러온 저장된 선택 영역이 없습니다 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index f680fe99e..c24a2da14 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -601,6 +601,8 @@ New Variable… Nowa zmienna… Unbind Variable Odłącz zmienną Delete Variable Usuń zmienną + Lock Variables Zablokuj zmienne + Unlock Variables Odblokuj zmienne New Data Set… Nowy zestaw danych… Edit Data Set… Edytuj zestaw danych… Delete Data Set Usuń zestaw danych @@ -4239,6 +4241,7 @@ imageTrace Outline Kontur @msg unknown tool or command `{key}` nieznane narzędzie lub polecenie `{key}` @msg unknown workspace (one of {_1}) nieznany obszar roboczy (jeden z: {_1}) @msg unsupported: {0} nieobsługiwane: {0} +@msg variables are locked zmienne są zablokowane @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files plików w wersji {0} nie można kompresować: skompresowane pliki otwierają tylko wersje {COMPRESSED_SINCE} i nowsze @msg width and height are at least 1 pixel szerokość i wysokość muszą wynosić co najmniej 1 piksel @msg workspace name can't be empty nazwa obszaru roboczego nie może być pusta diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 18c98489f..95ed680fd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2711,6 +2711,8 @@ New Variable… Nova variável… Unbind Variable Desassociar variável Delete Variable Excluir variável + Lock Variables Bloquear variáveis + Unlock Variables Desbloquear variáveis New Data Set… Novo conjunto de dados… Edit Data Set… Editar conjunto de dados… Delete Data Set Excluir conjunto de dados @@ -3702,6 +3704,7 @@ axis Both Ambos @msg unknown tool or command `{key}` ferramenta ou comando desconhecido `{key}` @msg unknown workspace (one of {_1}) espaço de trabalho desconhecido (um de {_1}) @msg unsupported: {0} não suportado: {0} +@msg variables are locked as variáveis estão bloqueadas @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files arquivos da versão {0} não podem ser compactados: somente a versão {COMPRESSED_SINCE} e posteriores abrem arquivos compactados @msg width and height are at least 1 pixel largura e altura têm pelo menos 1 pixel @msg workspace name can't be empty o nome do espaço de trabalho não pode estar vazio diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 473985077..d3e70d532 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3341,6 +3341,8 @@ mark-style Japanese Японский Variables Переменные New Variable Новая переменная Delete Variable Удалить переменную + Lock Variables Заблокировать переменные + Unlock Variables Разблокировать переменные List Variables Перечислить переменные Bind Variable Привязать переменную Unbind Variable Отвязать переменную @@ -4142,6 +4144,7 @@ axis Both Оба @msg unknown tool or command `{key}` неизвестный инструмент или команда `{key}` @msg unknown workspace (one of {_1}) неизвестная рабочая среда (одна из: {_1}) @msg unsupported: {0} не поддерживается: {0} +@msg variables are locked переменные заблокированы @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files файлы версии {0} не могут быть сжаты: только версия {COMPRESSED_SINCE} и новее открывают сжатые файлы @msg width and height are at least 1 pixel ширина и высота должны быть не менее 1 пикселя @msg workspace name can't be empty имя рабочей среды не может быть пустым diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index d09d3b30d..d0cf60a70 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3266,6 +3266,8 @@ Variables Variabler New Variable Ny variabel Delete Variable Radera variabel + Lock Variables Lås variabler + Unlock Variables Lås upp variabler List Variables Lista variabler Bind Variable Bind variabel Unbind Variable Koppla bort variabel @@ -4045,6 +4047,7 @@ axis Both Båda @msg unknown tool or command `{key}` okänt verktyg eller kommando `{key}` @msg unknown workspace (one of {_1}) okänd arbetsyta (en av {_1}) @msg unsupported: {0} stöds inte: {0} +@msg variables are locked variablerna är låsta @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files filer i version {0} kan inte komprimeras: endast version {COMPRESSED_SINCE} och senare kan öppna komprimerade filer @msg width and height are at least 1 pixel bredd och höjd är minst 1 pixel @msg workspace name can't be empty arbetsytans namn får inte vara tomt diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index bf4c2c3ef..c8344f116 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3338,6 +3338,8 @@ effect Points Точки Variables Змінні New Variable Нова змінна Delete Variable Видалити змінну + Lock Variables Заблокувати змінні + Unlock Variables Розблокувати змінні List Variables Перерахувати змінні Bind Variable Прив'язати змінну Unbind Variable Відв'язати змінну @@ -4130,6 +4132,7 @@ axis Both Обидві @msg unknown tool or command `{key}` невідомий інструмент або команда `{key}` @msg unknown workspace (one of {_1}) невідоме робоче середовище (одна з: {_1}) @msg unsupported: {0} не підтримується: {0} +@msg variables are locked змінні заблоковані @msg version {0} files can't be compressed: only version {COMPRESSED_SINCE} and later open compressed files файли версії {0} не можуть бути стиснуті: тільки версія {COMPRESSED_SINCE} і новіші відкривають стислі файли @msg width and height are at least 1 pixel ширина та висота повинні бути не менше 1 пікселя @msg workspace name can't be empty ім’я робочого середовища не може бути порожнім diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 1c7209bc6..6a870d46d 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1903,6 +1903,7 @@ file-filter Templates Mẫu Lock Others Khóa các mục khác Lock Slices Khóa lát cắt Lock Station Point Khóa điểm trạm + Lock Variables Khóa biến số Long Lines Nét dài Long Strokes Nét vẽ dài Longest: Dài nhất: @@ -3634,6 +3635,7 @@ mark-style Japanese Kiểu Nhật Unlock All Layers Mở khóa tất cả lớp Unlock Grid Mở khóa lưới Unlock Guides Mở khóa đường căn + Unlock Variables Mở khóa biến số Unnamed Objects Đối tượng chưa đặt tên Unprintable margin: Lề không in được: Unsharp Mask Tăng độ sắc nét @@ -4485,6 +4487,7 @@ theme Appearance Giao diện @msg unsupported: {0} không được hỗ trợ: {0} @msg Updated {_1} image(s) Đã cập nhật {_1} ảnh @msg Updated {_1} linked file(s) changed on disk Đã cập nhật {_1} tệp liên kết đã thay đổi trên đĩa +@msg variables are locked biến số bị khóa @msg VectorCraft doesn't search other apps' or the system's folders: {path} VectorCraft không tìm kiếm trong thư mục của ứng dụng khác hoặc hệ thống: {path} @msg VectorCraft doesn't search the Shared or Public folder in Users: {path} VectorCraft không tìm kiếm thư mục Shared hoặc Public trong Users: {path} @msg VectorCraft failed to start: {e} VectorCraft khởi động thất bại: {e} diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2f45a416e..6a306bf0c 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3204,6 +3204,9 @@ mark-style Japanese 日式 Variables 变量 New Variable 新建变量 Delete Variable 删除变量 + Lock Variables 锁定变量 + Unlock Variables 解锁变量 +@msg variables are locked 变量已锁定 List Variables 列出变量 Bind Variable 绑定变量 Unbind Variable 解除变量绑定 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 15cebb0e8..57c5e9436 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3325,6 +3325,8 @@ mark-style Japanese 日式 Variables 變數 New Variable 新增變數 Delete Variable 刪除變數 + Lock Variables 鎖定變數 + Unlock Variables 解除鎖定變數 List Variables 列出變數 Bind Variable 綁定變數 Unbind Variable 解除變數綁定 diff --git a/crates/ui-egui/src/panels/variables.rs b/crates/ui-egui/src/panels/variables.rs index a256e10af..96f32e132 100644 --- a/crates/ui-egui/src/panels/variables.rs +++ b/crates/ui-egui/src/panels/variables.rs @@ -136,6 +136,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let active = sets.iter().find(|s| s.active); let Picked { selected, has_selection, selection_bound, single_type, single_image, selection_graph } = picked(st); let highlighted = highlighted_row(st); + // Lock Variables: no variable is defined, deleted or renamed until they are unlocked. + let locked = st.doc.variables.locked; // The current data set across the top: its name, and the arrows that step through them. ui.horizontal(|ui| { @@ -181,7 +183,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if r.clicked() { click_row(app, &row.name, &row.bound); } - if r.double_clicked() { + if r.double_clicked() && !locked { crate::dialogs::variables::open(app, "variable.rename", Some(&row.name)); } }); @@ -204,31 +206,37 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } - // The bottom bar: make the selection dynamic, and define and delete what the lists show. + // The bottom bar: make the selection dynamic, define and delete what the lists show, and + // lock the variables. ui.separator(); ui.horizontal(|ui| { - if widgets::icon_button_enabled(ui, "type", tl!("Make Text Dynamic"), false, single_type, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "type", tl!("Make Text Dynamic"), false, single_type && !locked, 24.0).clicked() { app.run("variable.makeTextDynamic", json!({})).ok(); } - if widgets::icon_button_enabled(ui, "eye", tl!("Make Visibility Dynamic"), false, has_selection, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "eye", tl!("Make Visibility Dynamic"), false, has_selection && !locked, 24.0).clicked() { app.run("variable.makeVisibilityDynamic", json!({})).ok(); } - if widgets::icon_button_enabled(ui, "image", tl!("Make Image Dynamic"), false, single_image, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "image", tl!("Make Image Dynamic"), false, single_image && !locked, 24.0).clicked() { app.run("variable.makeImageDynamic", json!({})).ok(); } - if widgets::icon_button_enabled(ui, "chart-column", tl!("Make Graph Dynamic"), false, selection_graph, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "chart-column", tl!("Make Graph Dynamic"), false, selection_graph && !locked, 24.0).clicked() { app.run("variable.makeGraphDynamic", json!({})).ok(); } - if widgets::icon_button(ui, "plus", tl!("New Variable…"), false, 24.0).clicked() { + if widgets::icon_button_enabled(ui, "plus", tl!("New Variable…"), false, !locked, 24.0).clicked() { crate::menus::invoke(app, "variable.define", json!({})); } if widgets::icon_button_enabled(ui, "link-2-off", tl!("Unbind Variable"), false, selection_bound, 24.0).clicked() { app.run("variable.unbind", json!({"ids": selected})).ok(); } // The bin deletes the highlighted variable only, as the reference panel's does. - if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Variable"), false, highlighted.is_some(), 24.0).clicked() { + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Variable"), false, highlighted.is_some() && !locked, 24.0).clicked() { delete_highlighted(app); } + // The lock button is drawn pressed while the variables are locked. + let tip = if locked { tl!("Unlock Variables") } else { tl!("Lock Variables") }; + if widgets::icon_button(ui, "lock", tip, locked, 24.0).clicked() { + app.run("variable.lock", json!({})).ok(); + } }); ui.horizontal(|ui| { // Capture records what the art holds now as a new row (paste: take it in); Update @@ -254,33 +262,38 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let highlighted = highlighted_row(st); let highlighted_bound: Vec = highlighted.as_ref().map(|h| bound_objects(st, h)).unwrap_or_default(); let Picked { selected, has_selection, selection_bound, single_type, single_image, selection_graph } = picked(st); + let locked = st.doc.variables.locked; - if widgets::menu_item(ui, tl!("New Variable…"), true, false) { + if widgets::menu_item(ui, tl!("New Variable…"), !locked, false) { crate::menus::invoke(app, "variable.define", json!({})); } // Variable Options renames the highlighted variable, else the first. - if widgets::menu_item(ui, tl!("Variable Options…"), !vars.is_empty(), false) { + if widgets::menu_item(ui, tl!("Variable Options…"), !vars.is_empty() && !locked, false) { let name = highlighted.clone().or_else(|| vars.first().cloned()).unwrap_or_default(); crate::dialogs::variables::open(app, "variable.rename", Some(&name)); } - if widgets::menu_item(ui, tl!("Make Text Dynamic"), single_type, false) { + if widgets::menu_item(ui, tl!("Make Text Dynamic"), single_type && !locked, false) { app.run("variable.makeTextDynamic", json!({})).ok(); } - if widgets::menu_item(ui, tl!("Make Visibility Dynamic"), has_selection, false) { + if widgets::menu_item(ui, tl!("Make Visibility Dynamic"), has_selection && !locked, false) { app.run("variable.makeVisibilityDynamic", json!({})).ok(); } - if widgets::menu_item(ui, tl!("Make Image Dynamic"), single_image, false) { + if widgets::menu_item(ui, tl!("Make Image Dynamic"), single_image && !locked, false) { app.run("variable.makeImageDynamic", json!({})).ok(); } - if widgets::menu_item(ui, tl!("Make Graph Dynamic"), selection_graph, false) { + if widgets::menu_item(ui, tl!("Make Graph Dynamic"), selection_graph && !locked, false) { app.run("variable.makeGraphDynamic", json!({})).ok(); } if widgets::menu_item(ui, tl!("Unbind Variable"), selection_bound, false) { app.run("variable.unbind", json!({"ids": selected})).ok(); } - if widgets::menu_item(ui, tl!("Delete Variable"), highlighted.is_some(), false) { + if widgets::menu_item(ui, tl!("Delete Variable"), highlighted.is_some() && !locked, false) { delete_highlighted(app); } + let lock = if locked { tl!("Unlock Variables") } else { tl!("Lock Variables") }; + if widgets::menu_item(ui, lock, true, false) { + app.run("variable.lock", json!({})).ok(); + } ui.separator(); if widgets::menu_item(ui, tl!("Capture Data Set"), true, false) { app.run("dataset.capture", json!({})).ok(); @@ -398,6 +411,20 @@ mod tests { } } + /// The panel menu offers Lock Variables, and Unlock Variables once they are locked. + #[test] + fn the_panel_menu_locks_and_unlocks_the_variables() { + let mut app = app_with_data(); + let text = crate::tests_labels::painted_text(&mut app, menu); + assert!(text.contains("Lock Variables") && !text.contains("Unlock Variables"), "{text}"); + app.run("variable.lock", json!({})).unwrap(); + let text = crate::tests_labels::painted_text(&mut app, menu); + assert!(text.contains("Unlock Variables"), "{text}"); + // The panel still draws while they are locked. + let text = crate::tests_labels::painted_text(&mut app, show); + assert!(text.contains("Name") && text.contains("Data Set 1"), "{text}"); + } + /// What the panel acts on is the highlight, and only while it names a variable: a stale one /// (deleted elsewhere, undone) highlights nothing, so Delete and Options… stay disabled. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index fe4fdd0f5..c48f2b99b 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1125,7 +1125,11 @@ already walks Image Trace. Short agent entry points: - Variables: `variable.list`, `variable.define {name, kind: text|visibility|image|graph}`, `variable.bind {variable, ids?}`, `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.makeGraphDynamic`, `variable.unbind`, `variable.rename`, `variable.delete`, `variable.highlight` (image values are file paths, graph - values CSV as `graph.setData`). + values CSV as `graph.setData`). `variable.lock {locked?}` (Lock Variables) locks or unlocks the variables; + without `locked` it toggles, and the lock is saved with the document. While the variables are locked, + `variable.define`, `variable.delete`, `variable.rename` and the four `variable.make…Dynamic` commands fail with + "variables are locked"; `variable.bind`, `variable.unbind` and the `dataset.*` commands still run, and + `variable.list` reports `locked`. - Data sets: `dataset.list`, `dataset.new {name, values?}`, `dataset.set {name, values?}`, `dataset.rename`, `dataset.delete`, `dataset.capture`, `dataset.update`, `dataset.select {name}`, `dataset.next` / `dataset.prev` (each `dataset.select` / next / prev is one undo step over every bound object), `dataset.import {path|data|dataBase64|csv}` / diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index a227243e9..924811581 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -425,6 +425,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Variables: Update Data Set writes what the art holds into the active data set for the variables that data set holds, and keeps the data set's value for a variable whose objects are locked or inside a locked layer or group; `dataset.update {all: true}` also adds the other variables that the art reports a value for. - Variables: `dataset.select`, `dataset.next` and `dataset.prev` rewrite type and apply the other values on hidden objects and in hidden layers and groups, which stay hidden unless a visibility value shows them, so a layer shown later holds the selected data set's text; objects that are locked or in a locked layer or group are skipped. - Variables: `dataset.select`, `dataset.next` and `dataset.prev` name the area type and type on a path whose text they rewrote and that doesn't fit, in the reply's `overflow` key, as `text.setText` does. + - Variables: Lock Variables (`variable.lock`, in the Variables panel's bottom bar and menu) stops variables from being defined, deleted or renamed until they are unlocked, while objects can still be bound and unbound and data sets still change and apply; the lock is saved with the document. - Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command. - Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\` and `:` in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used. - Variables (data merge): text, visibility and image variables bound to objects (an object may carry several), dataset rows applied in one undo step (`variable.*`, `dataset.*`) from a Variables panel and Window › Variables, with dialogs to define, bind and edit, Make Text/Visibility/Image Dynamic, Capture/Update Data Set, CSV import/export (`dataset.import` / `dataset.export`), and a row in italics when an edit leaves the art not matching it; an image dataset value is a file path and apply keeps the object's bounds; bindings let go of deleted art and locked art is left alone; every command, like the rest, is drivable over MCP and the CLI.