Merge pull request #2631 from dotCooCoo/feat/lock-variables

Variables: Lock Variables stops variables from being defined, deleted or renamed
This commit is contained in:
BFlat
2026-10-17 17:16:35 +07:00
committed by GitHub
25 changed files with 233 additions and 32 deletions
+4
View File
@@ -116,6 +116,10 @@ pub struct Variables {
pub bindings: BTreeMap<NodeId, BTreeSet<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_dataset: Option<String>,
/// 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 {
+52 -16
View File
@@ -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<String> {
named(str_param(p, "name"), "name", cmd)
}
@@ -232,7 +245,21 @@ fn list(s: &mut Session, _p: &Value) -> Result<Value> {
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<Value> {
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<Value> {
@@ -812,8 +839,8 @@ pub fn specs() -> Vec<CommandSpec> {
"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<CommandSpec> {
"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<CommandSpec> {
"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<CommandSpec> {
"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<CommandSpec> {
"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<CommandSpec> {
"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<CommandSpec> {
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",
+73
View File
@@ -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::<Vec<_>>()
};
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]
+3
View File
@@ -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 името на работното пространство не може да бъде празно
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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ý
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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 το όνομα χώρου εργασίας δεν μπορεί να είναι κενό
Can't render this file because it contains an unexpected character in line 1923 and column 19.
+3
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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ä
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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 保存した選択範囲を呼び出していません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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 불러온 저장된 선택 영역이 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -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 имя рабочей среды не может быть пустым
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -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 ім’я робочого середовища не може бути порожнім
Can't render this file because it contains an unexpected character in line 723 and column 40.
+3
View File
@@ -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}
Can't render this file because it contains an unexpected character in line 3852 and column 10.
+3
View File
@@ -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 解除变量绑定
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3325,6 +3325,8 @@ mark-style Japanese 日式
Variables 變數
New Variable 新增變數
Delete Variable 刪除變數
Lock Variables 鎖定變數
Unlock Variables 解除鎖定變數
List Variables 列出變數
Bind Variable 綁定變數
Unbind Variable 解除變數綁定
Can't render this file because it contains an unexpected character in line 6 and column 47.
+42 -15
View File
@@ -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<u64> = 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]
+5 -1
View File
@@ -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}` /
+1
View File
@@ -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.