mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7786 from dotCooCoo/feat/variable-options-kind
Variables: Variable Options changes a variable's kind as well as its name
This commit is contained in:
@@ -245,39 +245,92 @@ fn define(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"name": name, "kind": kind.label()}))
|
||||
}
|
||||
|
||||
/// Whether the object `id`, bound to the variable `name`, can stay bound to it once the variable
|
||||
/// is of `kind`: a text variable drives type that holds no other text variable, an image variable
|
||||
/// drives an image object, a graph variable drives a graph root, and a visibility variable drives
|
||||
/// any object. Locks and hidden layers don't count, as they don't unbind anything else either.
|
||||
fn keeps_binding(doc: &Document, id: NodeId, name: &str, kind: VariableKind) -> bool {
|
||||
let Some(n) = doc.node(id) else { return false };
|
||||
match kind {
|
||||
VariableKind::Text => {
|
||||
matches!(n.kind, NodeKind::Text(_))
|
||||
&& !doc.variables.bindings.get(&id).is_some_and(|bound| {
|
||||
doc.variables.variables.iter().any(|v| v.kind == VariableKind::Text && v.name != name && bound.contains(&v.name))
|
||||
})
|
||||
}
|
||||
VariableKind::Visibility => true,
|
||||
VariableKind::Image => matches!(n.kind, NodeKind::Image(_)),
|
||||
VariableKind::Graph => n.extras().graph.is_some(),
|
||||
VariableKind::Unknown => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Variable Options: rename a variable (`newName`), change its kind (`kind`), or both, in one undo
|
||||
/// step, carrying its bindings and its values in every data set to the new name. A kind change
|
||||
/// unbinds the objects that can't hold the new kind ([`keeps_binding`]) and drops the variable's
|
||||
/// value in every data set. A call that changes nothing adds no undo step.
|
||||
fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "variable.rename";
|
||||
let from = var_name(p, C)?;
|
||||
let to = named(p.get("newName").and_then(Value::as_str), "newName", C)?;
|
||||
let st = s.doc()?;
|
||||
if st.doc.variables.variable(&from).is_none() {
|
||||
return Err(bad(C, format!("no variable named `{from}`")));
|
||||
// An omitted key is no change; a null, like any other value that isn't a name or a kind, is refused.
|
||||
let to = match p.get("newName") {
|
||||
None => None,
|
||||
Some(v) => Some(named(v.as_str(), "newName", C)?),
|
||||
};
|
||||
let kind = match p.get("kind") {
|
||||
None => None,
|
||||
Some(v) => Some(v.as_str().and_then(VariableKind::parse).ok_or_else(|| bad(C, "`kind` must be `text`, `visibility`, `image` or `graph`"))?),
|
||||
};
|
||||
if to.is_none() && kind.is_none() {
|
||||
return Err(bad(C, "missing `newName` or `kind`"));
|
||||
}
|
||||
if st.doc.variables.variable(&to).is_some() {
|
||||
let doc = &s.doc()?.doc;
|
||||
let Some(old) = doc.variables.variable(&from) else {
|
||||
return Err(bad(C, format!("no variable named `{from}`")));
|
||||
};
|
||||
let to = to.unwrap_or_else(|| from.clone());
|
||||
if to != from && doc.variables.variable(&to).is_some() {
|
||||
return Err(bad(C, format!("variable `{to}` already exists")));
|
||||
}
|
||||
s.edit("Rename Variable", |d, _| {
|
||||
let v = d.variables.variables.iter_mut().find(|v| v.name == from).ok_or_else(|| bad(C, format!("no variable named `{from}`")))?;
|
||||
let kind = kind.unwrap_or(old.kind);
|
||||
let retyped = kind != old.kind;
|
||||
let unbound: Vec<NodeId> =
|
||||
if retyped { doc.variables.objects_of(&from).into_iter().filter(|id| !keeps_binding(doc, *id, &from, kind)).collect() } else { Vec::new() };
|
||||
let label = if retyped { "Variable Options" } else { "Rename Variable" };
|
||||
let dropped = edit_variables(s, label, |vars| {
|
||||
let v = vars.variables.iter_mut().find(|v| v.name == from).ok_or_else(|| bad(C, format!("no variable named `{from}`")))?;
|
||||
v.name = to.clone();
|
||||
for bound in d.variables.bindings.values_mut() {
|
||||
v.kind = kind;
|
||||
for id in &unbound {
|
||||
vars.unbind(*id, &from);
|
||||
}
|
||||
for bound in vars.bindings.values_mut() {
|
||||
if bound.remove(&from) {
|
||||
bound.insert(to.clone());
|
||||
}
|
||||
}
|
||||
for ds in &mut d.variables.datasets {
|
||||
if let Some(value) = ds.values.remove(&from) {
|
||||
ds.values.insert(to.clone(), value);
|
||||
let mut dropped = 0;
|
||||
for ds in &mut vars.datasets {
|
||||
match ds.values.remove(&from) {
|
||||
// A rename alone carries every value, one that doesn't fit the kind included; a kind
|
||||
// change drops them all, as none was given for the new kind.
|
||||
Some(value) if !retyped => {
|
||||
ds.values.insert(to.clone(), value);
|
||||
}
|
||||
Some(_) => dropped += 1,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(dropped)
|
||||
})?;
|
||||
// The highlight names a variable, so it follows the rename rather than going stale.
|
||||
// The highlight names a variable, so it follows the rename rather than going stale. A call
|
||||
// that leaves the name as it was leaves the revision alone too.
|
||||
let st = s.doc_mut()?;
|
||||
if st.variables_highlight.as_deref() == Some(from.as_str()) {
|
||||
if to != from && st.variables_highlight.as_deref() == Some(from.as_str()) {
|
||||
st.variables_highlight = Some(to.clone());
|
||||
st.revision += 1;
|
||||
}
|
||||
st.revision += 1;
|
||||
Ok(json!({"from": from, "name": to}))
|
||||
Ok(json!({"from": from, "name": to, "kind": kind.label(), "unbound": unbound.iter().map(|i| i.0).collect::<Vec<_>>(), "droppedValues": dropped}))
|
||||
}
|
||||
|
||||
/// Make Text / Visibility / Image / Graph Dynamic: define a variable of that kind and bind the
|
||||
@@ -837,7 +890,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Rename Variable",
|
||||
["Window", "Variables"],
|
||||
None,
|
||||
"{name, newName} rename a variable, with its bindings and its values in every dataset; refused while the variables are locked",
|
||||
"{name, newName?, kind?: text|visibility|image|graph} Variable Options: rename a variable, with its bindings and its values in every dataset, change its kind, or both, in one undo step (none when the name and the kind are the ones it has). A kind change unbinds every object that can't hold the new kind (a text variable keeps type that has no other text variable, an image variable keeps image objects, a graph variable keeps graph roots, a visibility variable keeps every object) and drops the variable's values in every dataset; locked and hidden objects keep their binding when they can hold it. Needs `newName` or `kind`, and a null or empty one is refused; refused while the variables are locked → {from, name, kind, unbound: [id…], droppedValues}",
|
||||
variables_unlocked,
|
||||
rename
|
||||
),
|
||||
|
||||
@@ -1063,6 +1063,110 @@ fn renaming_a_missing_name_onto_a_taken_one_reports_the_missing_name() {
|
||||
assert!(e.contains("no dataset named `Nope`"), "{e}");
|
||||
}
|
||||
|
||||
/// Variable Options changes a variable's kind: the objects that can't hold the new kind are
|
||||
/// unbound, the ones that can (locked ones too) stay bound, every value of the old kind goes, and
|
||||
/// the change is one undo step.
|
||||
#[test]
|
||||
fn variable_options_changes_the_kind_and_unbinds_what_cant_hold_it() {
|
||||
let mut s = session();
|
||||
let t = text(&mut s, "Alice");
|
||||
let r = rect(&mut s);
|
||||
let other = text(&mut s, "Held");
|
||||
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Title", "kind": "text"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Show", "ids": [t.0, r.0, other.0]})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Title", "ids": [other.0]})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "A", "values": {"Show": false, "Title": "Bob"}})).unwrap();
|
||||
s.execute("dataset.new", &json!({"name": "B", "values": {"Show": true}})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [t.0]})).unwrap();
|
||||
s.execute("object.lock", &json!({})).unwrap();
|
||||
let steps = |s: &Session| s.doc().unwrap().history.undo.len();
|
||||
let before = steps(&s);
|
||||
|
||||
let v = s.execute("variable.rename", &json!({"name": "Show", "kind": "text"})).unwrap();
|
||||
// The rectangle isn't type, and the other type object already has the text variable Title.
|
||||
assert_eq!(v["unbound"], json!([r.0, other.0]));
|
||||
assert_eq!((v["name"].clone(), v["kind"].clone(), v["droppedValues"].clone()), (json!("Show"), json!("text"), json!(2)));
|
||||
assert_eq!(steps(&s), before + 1, "one undo step");
|
||||
let vars = &s.doc().unwrap().doc.variables;
|
||||
assert_eq!(vars.objects_of("Show"), [t], "the locked type object stays bound");
|
||||
assert_eq!(vars.objects_of("Title"), [other], "the other variable keeps its binding");
|
||||
let sets = s.execute("dataset.list", &json!({})).unwrap();
|
||||
assert_eq!((sets["datasets"][0]["values"].clone(), sets["datasets"][1]["values"].clone()), (json!({"Title": {"text": "Bob"}}), json!({})));
|
||||
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let vars = &s.doc().unwrap().doc.variables;
|
||||
assert_eq!(vars.variable("Show").map(|v| v.kind), Some(vectorcraft_doc::VariableKind::Visibility));
|
||||
assert_eq!(vars.objects_of("Show"), [t, r, other]);
|
||||
assert_eq!(vars.dataset("A").and_then(|d| d.values.get("Show")), Some(&vectorcraft_doc::DataValue::Visible(false)));
|
||||
|
||||
// A new name and a new kind together are one undo step, and every visibility binding stays.
|
||||
s.execute("variable.rename", &json!({"name": "Title", "newName": "Heading", "kind": "visibility"})).unwrap();
|
||||
assert_eq!(steps(&s), before + 1, "one step after the undo");
|
||||
let vars = &s.doc().unwrap().doc.variables;
|
||||
assert_eq!(
|
||||
(vars.variable("Heading").map(|v| v.kind), vars.objects_of("Heading")),
|
||||
(Some(vectorcraft_doc::VariableKind::Visibility), vec![other])
|
||||
);
|
||||
// An image variable keeps only image objects.
|
||||
let v = s.execute("variable.rename", &json!({"name": "Show", "kind": "image"})).unwrap();
|
||||
assert_eq!(v["unbound"], json!([t.0, r.0, other.0]));
|
||||
}
|
||||
|
||||
/// Variable Options with the variable's own name and kind changes nothing and adds no undo step;
|
||||
/// a bad kind, a taken name, no change asked for and locked variables are refused.
|
||||
#[test]
|
||||
fn variable_options_refuses_bad_params_and_skips_a_change_that_changes_nothing() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s);
|
||||
s.execute("variable.define", &json!({"name": "Show", "kind": "visibility"})).unwrap();
|
||||
s.execute("variable.define", &json!({"name": "Other", "kind": "text"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"variable": "Show", "ids": [r.0]})).unwrap();
|
||||
let steps = |s: &Session| s.doc().unwrap().history.undo.len();
|
||||
let before = steps(&s);
|
||||
for p in [
|
||||
json!({"name": "Show", "newName": "Show"}),
|
||||
json!({"name": "Show", "kind": "visibility"}),
|
||||
json!({"name": "Show", "newName": "Show", "kind": "visible"}),
|
||||
] {
|
||||
let v = s.execute("variable.rename", &p).unwrap();
|
||||
assert_eq!((v["unbound"].clone(), v["droppedValues"].clone()), (json!([]), json!(0)), "{p}");
|
||||
assert_eq!(steps(&s), before, "{p}: no undo step");
|
||||
}
|
||||
for (p, want) in [
|
||||
(json!({"name": "Show", "kind": "unknown"}), "`kind` must be"),
|
||||
(json!({"name": "Show", "kind": "number"}), "`kind` must be"),
|
||||
(json!({"name": "Show", "kind": 1}), "`kind` must be"),
|
||||
(json!({"name": "Show", "newName": "New", "kind": null}), "`kind` must be"),
|
||||
(json!({"name": "Show", "newName": null, "kind": "text"}), "missing `newName`"),
|
||||
(json!({"name": "Show", "newName": " ", "kind": "text"}), "missing `newName`"),
|
||||
(json!({"name": "Show"}), "missing `newName` or `kind`"),
|
||||
(json!({"name": "Show", "newName": "Other", "kind": "text"}), "already exists"),
|
||||
(json!({"name": "Ghost", "kind": "text"}), "no variable named `Ghost`"),
|
||||
] {
|
||||
let e = s.execute("variable.rename", &p).unwrap_err().to_string();
|
||||
assert!(e.contains(want), "{p}: {e}");
|
||||
}
|
||||
// A rename alone carries a value that doesn't fit the kind, as a file may hold one.
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc)
|
||||
.variables
|
||||
.datasets
|
||||
.push(vectorcraft_doc::DataSet { name: "Odd".into(), values: [("Show".to_string(), vectorcraft_doc::DataValue::Text("x".into()))].into() });
|
||||
s.execute("variable.rename", &json!({"name": "Show", "newName": "Shown"})).unwrap();
|
||||
let odd = s.doc().unwrap().doc.variables.dataset("Odd").and_then(|d| d.values.get("Shown")).cloned();
|
||||
assert_eq!(odd, Some(vectorcraft_doc::DataValue::Text("x".into())));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
// A kind change drops it, though it fits the new kind: nobody gave it for that kind.
|
||||
let v = s.execute("variable.rename", &json!({"name": "Show", "kind": "text"})).unwrap();
|
||||
assert_eq!(v["droppedValues"], 1);
|
||||
assert!(s.doc().unwrap().doc.variables.dataset("Odd").is_some_and(|d| d.values.is_empty()));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("variable.lock", &json!({"locked": true})).unwrap();
|
||||
assert!(s.execute("variable.rename", &json!({"name": "Show", "kind": "text"})).is_err(), "refused while the variables are locked");
|
||||
let vars = &s.doc().unwrap().doc.variables;
|
||||
assert_eq!((vars.variable("Show").map(|v| v.kind), vars.objects_of("Show")), (Some(vectorcraft_doc::VariableKind::Visibility), vec![r]));
|
||||
}
|
||||
|
||||
/// `dataset.set` is how a value changes after the row exists: replacing it must not need the
|
||||
/// dataset to be deleted and rebuilt (which loses its place in the list and the active one).
|
||||
#[test]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! The Variables dialogs (`variable.define`, `variable.bind`, `dataset.new`, `dataset.set`
|
||||
//! and `dataset.select`), opened from Window › Variables or the command palette.
|
||||
//! The Variables dialogs (`variable.define`, `variable.rename` as Variable Options, `variable.bind`,
|
||||
//! `dataset.new`, `dataset.set`, `dataset.rename` and `dataset.select`), opened from Window ›
|
||||
//! Variables or the command palette. Variable Options shows the variable's name and kind, and OK
|
||||
//! changes either or both.
|
||||
//!
|
||||
//! One dialog covers all of them: what it shows is what the command takes. A variable is a
|
||||
//! name and a kind; a dataset is a name and one field per variable — a text field for a
|
||||
@@ -24,6 +26,10 @@ pub const KIND: &str = "variables";
|
||||
/// renames.
|
||||
const EDITED: &str = "__dataset";
|
||||
|
||||
/// The field of Variable Options that names the variable its Name and Kind fields belong to.
|
||||
/// When the Variable field names another one, the next frame fills them from that one.
|
||||
const FILLED: &str = "__filled";
|
||||
|
||||
/// The kinds a variable can have, as `variable.define` names them.
|
||||
const KINDS: [&str; 4] = ["text", "visibility", "image", "graph"];
|
||||
|
||||
@@ -50,7 +56,7 @@ fn opened(id: &str) -> Option<&'static str> {
|
||||
}
|
||||
|
||||
/// The row `id` renames, which the dialog offers as a dropdown when the document has more than
|
||||
/// one. Variable Options renames the one the panel has highlighted, so it starts there.
|
||||
/// one. Variable Options edits the one the panel has highlighted, so it starts there.
|
||||
fn rename_of(id: &str) -> Option<&'static str> {
|
||||
Some(match id {
|
||||
"variable.rename" => "variable",
|
||||
@@ -116,8 +122,14 @@ pub fn open(app: &mut VectorcraftApp, id: &str, preselect: Option<&str>) -> bool
|
||||
app.status(if key == "variable" { tl!("no variable to rename").to_string() } else { tl!("no data set to rename").to_string() });
|
||||
return true;
|
||||
}
|
||||
fields.insert(key.into(), json!(preselect.filter(|p| names.contains(p)).or(names.first().copied()).unwrap_or_default()));
|
||||
fields.insert("newName".into(), json!(""));
|
||||
let row = preselect.filter(|p| names.contains(p)).or(names.first().copied()).unwrap_or_default();
|
||||
fields.insert(key.into(), json!(row));
|
||||
if key == "variable" {
|
||||
// Variable Options starts on the variable's own name and kind.
|
||||
fill_variable_options(&vars, row, &mut fields);
|
||||
} else {
|
||||
fields.insert("newName".into(), json!(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
fields.insert("__command".into(), json!(id));
|
||||
@@ -126,6 +138,16 @@ pub fn open(app: &mut VectorcraftApp, id: &str, preselect: Option<&str>) -> bool
|
||||
true
|
||||
}
|
||||
|
||||
/// Variable Options' Name and Kind fields for the variable `name`: its name, and its kind as
|
||||
/// `variable.rename` takes it (empty for a variable of unknown kind, which OK leaves as it is).
|
||||
/// [`FILLED`] records which variable they were filled for.
|
||||
fn fill_variable_options(vars: &Variables, name: &str, fields: &mut serde_json::Map<String, Value>) {
|
||||
let kind = vars.variable(name).map(|v| v.kind).filter(|k| *k != VariableKind::Unknown).map(VariableKind::label).unwrap_or_default();
|
||||
fields.insert("newName".into(), json!(name));
|
||||
fields.insert("kind".into(), json!(kind));
|
||||
fields.insert(FILLED.into(), json!(name));
|
||||
}
|
||||
|
||||
/// Whether `id` opens one of these dialogs.
|
||||
pub fn opens(id: &str) -> bool {
|
||||
opened(id).is_some()
|
||||
@@ -181,13 +203,25 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
text(ui, d, "name", 220.0);
|
||||
ui.end_row();
|
||||
label(ui, tl!("Kind"));
|
||||
// The kinds by their translated names; the field keeps the command's own word.
|
||||
let labels = kind_labels();
|
||||
let shown = KINDS.iter().position(|k| *k == d.str("kind")).and_then(|i| labels.get(i)).copied().unwrap_or_default();
|
||||
if let Some(kind) = widgets::dropdown_names(ui, ("variables-dd", "kind"), shown, &labels, 220.0).and_then(|i| KINDS.get(i)) {
|
||||
d.fields.insert("kind".into(), json!(kind));
|
||||
kind_dropdown(ui, d);
|
||||
ui.end_row();
|
||||
}
|
||||
// Variable Options: which variable, and its name and kind; picking another variable
|
||||
// starts its fields over from that one.
|
||||
"variable.rename" => {
|
||||
label(ui, tl!("Variable"));
|
||||
dropdown(ui, d, "variable", &variable_names, 220.0);
|
||||
let picked = d.str("variable");
|
||||
if picked != d.str(FILLED) {
|
||||
fill_variable_options(vars, &picked, &mut d.fields);
|
||||
}
|
||||
ui.end_row();
|
||||
label(ui, tl!("Name"));
|
||||
text(ui, d, "newName", 220.0);
|
||||
ui.end_row();
|
||||
label(ui, tl!("Kind"));
|
||||
kind_dropdown(ui, d);
|
||||
ui.end_row();
|
||||
}
|
||||
"variable.bind" => {
|
||||
label(ui, tl!("Variable"));
|
||||
@@ -199,15 +233,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
dropdown(ui, d, "name", &dataset_names, 220.0);
|
||||
ui.end_row();
|
||||
}
|
||||
// A rename: which row, and its new name.
|
||||
"variable.rename" | "dataset.rename" => {
|
||||
let (key, label_text, names) = if d.str("__command") == "variable.rename" {
|
||||
("variable", tl!("Variable").to_string(), &variable_names)
|
||||
} else {
|
||||
("name", tl!("Data Set").to_string(), &dataset_names)
|
||||
};
|
||||
label(ui, &label_text);
|
||||
dropdown(ui, d, key, names, 220.0);
|
||||
// Rename Data Set: which row, and its new name.
|
||||
"dataset.rename" => {
|
||||
label(ui, tl!("Data Set"));
|
||||
dropdown(ui, d, "name", &dataset_names, 220.0);
|
||||
ui.end_row();
|
||||
label(ui, tl!("New Name"));
|
||||
text(ui, d, "newName", 220.0);
|
||||
@@ -245,6 +274,16 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The Kind dropdown: the kinds by their translated names, while the `kind` field keeps the
|
||||
/// command's own word. An empty field (a variable of unknown kind) shows no kind.
|
||||
fn kind_dropdown(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let labels = kind_labels();
|
||||
let shown = KINDS.iter().position(|k| *k == d.str("kind")).and_then(|i| labels.get(i)).copied().unwrap_or_default();
|
||||
if let Some(kind) = widgets::dropdown_names(ui, ("variables-dd", "kind"), shown, &labels, 220.0).and_then(|i| KINDS.get(i)) {
|
||||
d.fields.insert("kind".into(), json!(kind));
|
||||
}
|
||||
}
|
||||
|
||||
/// A text field for `key`, written back as it is typed.
|
||||
fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> egui::Response {
|
||||
let mut s = d.str(key);
|
||||
@@ -263,8 +302,26 @@ fn set_text(d: &mut Dialog, key: &str, s: String) {
|
||||
|
||||
/// Records that `field` was typed in or set by `ui.dialog.set`: it no longer holds the row's
|
||||
/// empty text value, so emptying it unsets the variable.
|
||||
///
|
||||
/// In Variable Options, setting the Variable field clears Name and Kind, which OK reads as the
|
||||
/// variable's own name and kind, until a frame fills them from it; setting Name or Kind ties them
|
||||
/// to the variable already named, so no frame fills them over.
|
||||
pub(super) fn note_set(d: &mut Dialog, field: &str) {
|
||||
d.fields.remove(&empty_marker(field));
|
||||
if d.str("__command") == "variable.rename" {
|
||||
match field {
|
||||
// Null rather than removed, so `ui.dialog.set` can still set them.
|
||||
"variable" => {
|
||||
d.fields.insert("newName".into(), Value::Null);
|
||||
d.fields.insert("kind".into(), Value::Null);
|
||||
}
|
||||
"newName" | "kind" => {
|
||||
let row = d.str("variable");
|
||||
d.fields.insert(FILLED.into(), json!(row));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A checkbox for `key`, written back as it is toggled, which screen readers call `name`.
|
||||
@@ -329,7 +386,18 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let p = match cmd.as_str() {
|
||||
"variable.define" => json!({"name": name, "kind": d.str("kind")}),
|
||||
"variable.bind" => json!({"variable": d.str("variable")}),
|
||||
"variable.rename" => json!({"name": d.str("variable"), "newName": d.str("newName")}),
|
||||
// A Name the Variable field just cleared keeps the variable's name (an emptied one is sent,
|
||||
// and refused), and a cleared or empty Kind (a variable of unknown kind) keeps its kind.
|
||||
"variable.rename" => {
|
||||
let variable = d.str("variable");
|
||||
let new_name = if matches!(d.fields.get("newName"), Some(Value::Null) | None) { variable.clone() } else { d.str("newName") };
|
||||
// Any other kind value goes to the command, which refuses one it doesn't know.
|
||||
match d.fields.get("kind") {
|
||||
None | Some(Value::Null) => json!({"name": variable, "newName": new_name}),
|
||||
Some(Value::String(k)) if k.is_empty() => json!({"name": variable, "newName": new_name}),
|
||||
Some(k) => json!({"name": variable, "newName": new_name, "kind": k}),
|
||||
}
|
||||
}
|
||||
"dataset.rename" => json!({"name": name, "newName": d.str("newName")}),
|
||||
"dataset.new" | "dataset.set" => {
|
||||
let values = app.session.active().map(|st| values_of(d, &st.doc.variables)).unwrap_or_else(|| json!({}));
|
||||
@@ -550,4 +618,55 @@ mod tests {
|
||||
open(&mut app, "dataset.rename", Some("Row 1"));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "Row 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_options_shows_the_name_and_kind_and_changes_either() {
|
||||
let mut app = app_with_variables();
|
||||
let kind_of = |app: &VectorcraftApp, name: &str| app.session.active().and_then(|st| st.doc.variables.variable(name).map(|v| v.kind));
|
||||
open(&mut app, "variable.rename", Some("Badge"));
|
||||
{
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.str("newName"), d.str("kind")), ("Badge".to_string(), "visibility".to_string()));
|
||||
}
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("kind".into(), json!("text"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(kind_of(&app, "Badge"), Some(VariableKind::Text));
|
||||
|
||||
// OK with nothing changed adds no undo step, and an emptied Name is refused.
|
||||
let undo = app.session.active().unwrap().history.undo.len();
|
||||
open(&mut app, "variable.rename", Some("Title"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().history.undo.len(), undo);
|
||||
open(&mut app, "variable.rename", Some("Title"));
|
||||
crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), "newName".into(), json!(""));
|
||||
assert!(crate::dialogs::confirm(&mut app).is_err());
|
||||
assert!(kind_of(&app, "Title").is_some(), "Title keeps its name");
|
||||
|
||||
// Setting the Variable field keeps that variable's own name and kind until Name or Kind is
|
||||
// set, and a frame fills them from it.
|
||||
open(&mut app, "variable.rename", Some("Title"));
|
||||
crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), "variable".into(), json!("Badge"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!((kind_of(&app, "Title"), kind_of(&app, "Badge")), (Some(VariableKind::Text), Some(VariableKind::Text)));
|
||||
open(&mut app, "variable.rename", Some("Title"));
|
||||
{
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
crate::dialogs::set_field(d, "variable".into(), json!("Badge"));
|
||||
assert!(crate::dialogs::accepts_field(d, "newName") && crate::dialogs::accepts_field(d, "kind"), "Name and Kind stay settable");
|
||||
crate::dialogs::set_field(d, "newName".into(), json!("Flag"));
|
||||
crate::dialogs::set_field(d, "kind".into(), json!("visibility"));
|
||||
}
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!((kind_of(&app, "Flag"), kind_of(&app, "Title")), (Some(VariableKind::Visibility), Some(VariableKind::Text)));
|
||||
|
||||
// A frame after the Variable field changes fills Name and Kind from that variable.
|
||||
open(&mut app, "variable.rename", Some("Title"));
|
||||
crate::dialogs::set_field(app.ui.dialog.as_mut().unwrap(), "variable".into(), json!("Flag"));
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::dialogs::show(&mut app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!((d.str("newName"), d.str("kind")), ("Flag".to_string(), "visibility".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
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.
|
||||
// Variable Options edits the name and kind of the highlighted variable, else the first.
|
||||
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));
|
||||
|
||||
+6
-1
@@ -3409,7 +3409,12 @@ already walks Image Trace. Short agent entry points:
|
||||
`variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.makeGraphDynamic`
|
||||
(`variable.bind` and `variable.makeTextDynamic` refuse type that already has another text variable and name it),
|
||||
`variable.unbind`, `variable.rename`, `variable.delete`, `variable.highlight` (image values are file paths, graph
|
||||
values CSV as `graph.setData`). `variable.lock {locked?}` (Lock Variables) locks or unlocks the variables;
|
||||
values CSV as `graph.setData`). `variable.rename {name, newName?, kind?}` is Variable Options: it renames a
|
||||
variable, changes its kind, or both, in one undo step, and adds none when the name and kind are the ones it has.
|
||||
A kind change unbinds each object that can't hold the new kind (a text variable keeps type with no other text
|
||||
variable, an image variable keeps image objects, a graph variable keeps graph roots, a visibility variable keeps
|
||||
every object; locks and hidden layers don't unbind anything) and drops the variable's values in every data set.
|
||||
That rule is VectorCraft's own. `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`, the four `variable.make…Dynamic` commands and a
|
||||
`variable.loadLibrary` that would delete or define a variable (below) fail with "variables are locked";
|
||||
|
||||
@@ -831,6 +831,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Variables: Import Data Sets… (`dataset.import`) defines the variables a file's header names that the document lacks, unbound. `#Shown` defines a visibility variable, `@Photo` defines an image variable, and a header without a prefix defines a text variable. A header that is a variable's exact name keeps naming it, and any other `%` header (graph data files) is refused.
|
||||
- Variables: Load Variable Library… in the Variables panel menu (`variable.loadLibrary`) replaces the document's variables and data sets with a variable library's, read from its XML file, in one undo step: every data set is deleted, and every variable is deleted with its bindings and values, except a variable the library also declares with the same kind, which stays bound so the library's data sets change its objects (#4136); then the library's other variables are defined, unbound: text, visibility, image (linked file) and graph variables, and variables without a known `trait` as unknown. Its data sets take text values from each value's `p` elements, one paragraph each, visibility values from `true` or `false`, image values from a file's absolute path or `file:` URL, and graph values from a `data` element of series names and rows.
|
||||
- Variables: Save Variable Library… in the Variables panel menu (`variable.saveLibrary`) writes the document's variables of every kind and its data sets as a variable library's XML file, which Load Variable Library reads back with the same variables, kinds, data set names and values.
|
||||
- Variables: Variable Options… in the Variables panel menu, or a double click on a variable, shows the variable's name and kind, and OK changes either or both in one undo step (`variable.rename {name, newName?, kind?}`), as the [Work with variables](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/work-with-variables.html) page describes. VectorCraft's own rule for a kind change: objects that can't hold the new kind are unbound, and the variable's values in every data set are dropped.
|
||||
- JSON-lines control channel with real egui pointer and keyboard injection.
|
||||
- Variables: a type object holds one text variable. `variable.bind` and Make Text Dynamic refuse a second one and name the text variable the object has; a visibility variable can still share the object, and a document saved with two text variables on one object opens with both.
|
||||
- Synthetic input validates complete gestures before queue mutation: at most 256 drag steps, 16 clicks and 1,024 pending events; up to 64 control requests per frame, with later requests retained for the next frame (#1709).
|
||||
|
||||
Reference in New Issue
Block a user