Merge pull request #7773 from dotCooCoo/feat/variables-panel-sort-columns

Variables: Variables, Objects and type columns sort the variable list
This commit is contained in:
BFlat
2026-11-15 14:43:04 +07:00
committed by GitHub
37 changed files with 459 additions and 38 deletions
+3
View File
@@ -899,6 +899,9 @@ pub use brushsym::reset_transformation_changes;
/// Data window reads files by it too).
pub use variablecmds::utf16_order_without_mark;
/// The Variables panel's sort (`variable.sort`) and its rows in that order.
pub use variablecmds::{VariableSort, VariableSortKey, variable_order};
/// Command ids kept only so older scripts keep working: `(alias, the command it duplicates)`.
/// They run as before but the command palette leaves them out (it lists the command once).
pub const ALIASES: &[(&str, &str)] = &[("object.convertDocumentColorMode", "file.documentColorMode")];
@@ -966,7 +966,7 @@ fn an_ai_file_opens_its_variable_library() {
{"name": "v_file", "kind": "image", "bindings": 0},
{"name": "v_graph", "kind": "graph", "bindings": 0},
{"name": "v_unbound", "kind": "unknown", "bindings": 0},
], "locked": true})
], "locked": true, "sort": {"by": "none", "descending": false}})
);
let r = s.execute("dataset.list", &json!({})).unwrap();
assert_eq!(r["active"], "ds_two", "{r}");
@@ -694,6 +694,7 @@ pub(super) const ALLOWED: &[&str] = &[
"variable.makeTextDynamic",
"variable.makeVisibilityDynamic",
"variable.rename",
"variable.sort",
"variable.unbind",
];
+139 -7
View File
@@ -330,18 +330,19 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
}
fn list(s: &mut Session, _p: &Value) -> Result<Value> {
let xml_id = s.prefs.identify_objects_by == "xmlId";
let st = s.doc()?;
let variables: Vec<Value> = st
.doc
.variables
.variables
.iter()
let vars = &st.doc.variables.variables;
let variables: Vec<Value> = variable_order(st, xml_id)
.into_iter()
.filter_map(|i| vars.get(i))
.map(|v| {
let bindings = st.doc.variables.objects_of(&v.name).len();
json!({"name": v.name, "kind": v.kind.label(), "bindings": bindings})
})
.collect();
Ok(json!({"variables": variables, "locked": st.doc.variables.locked}))
let sort = json!({"by": st.variables_sort.by.id(), "descending": st.variables_sort.descending});
Ok(json!({"variables": variables, "locked": st.doc.variables.locked, "sort": sort}))
}
/// Lock Variables: without `locked` it toggles. Setting the state the document already has
@@ -417,6 +418,120 @@ fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"variable": name}))
}
/// The column the Variables panel sorts its list by (`variable.sort`).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum VariableSortKey {
/// The order the variables were defined in.
#[default]
Defined,
/// The Variables column: the variable's name.
Name,
/// The Objects column: the name of the object the variable is bound to.
Object,
/// The type column: text, image, graph, visibility, then variables of unknown kind.
Type,
}
impl VariableSortKey {
/// The values `variable.sort` takes for `by`, in the order of [`Self::id`].
const IDS: [&'static str; 4] = ["name", "object", "type", "none"];
/// The value `variable.sort` takes for `by` and `variable.list` reports.
pub fn id(self) -> &'static str {
match self {
Self::Name => "name",
Self::Object => "object",
Self::Type => "type",
Self::Defined => "none",
}
}
fn parse(s: &str) -> Option<Self> {
match s {
"name" => Some(Self::Name),
"object" => Some(Self::Object),
"type" => Some(Self::Type),
"none" => Some(Self::Defined),
_ => None,
}
}
}
/// How the Variables panel orders its list: a column, and whether the order is reversed. Panel
/// state, not saved and not an undo step.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct VariableSort {
pub by: VariableSortKey,
pub descending: bool,
}
/// The place of a variable's kind in the order the type column sorts by.
fn kind_rank(kind: VariableKind) -> u8 {
match kind {
VariableKind::Text => 0,
VariableKind::Image => 1,
VariableKind::Graph => 2,
VariableKind::Visibility => 3,
VariableKind::Unknown => 4,
}
}
/// The Variables panel's rows in its sort order, as indices into the document's variables.
/// Names compare without regard to case. Sorted by object, a variable bound to nothing comes
/// first and one bound to several objects comes after those bound to one; `xml_id` names objects
/// by their XML ids, as the panel does with Identify Objects By set to XML ID. Ties go by name.
pub fn variable_order(st: &crate::DocState, xml_id: bool) -> Vec<usize> {
let vars = &st.doc.variables.variables;
let sort = st.variables_sort;
let mut order: Vec<usize> = (0..vars.len()).collect();
if sort.by != VariableSortKey::Defined {
order.sort_by_cached_key(|&i| {
let v = &vars[i];
let (rank, label) = match sort.by {
VariableSortKey::Object => match st.doc.variables.objects_of(&v.name).as_slice() {
[] => (0, String::new()),
[one] => (1, st.doc.node(*one).map(|n| n.identify_label(xml_id)).unwrap_or_default()),
_ => (2, String::new()),
},
VariableSortKey::Type => (kind_rank(v.kind), String::new()),
VariableSortKey::Name | VariableSortKey::Defined => (0, String::new()),
};
(rank, label.to_lowercase(), label, v.name.to_lowercase(), v.name.clone())
});
}
if sort.descending {
order.reverse();
}
order
}
/// Sort the Variables panel's list. Panel state, like `variable.highlight`: not saved and not an
/// undo step. Setting the order the panel already has changes nothing.
fn sort(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "variable.sort";
let key = "by";
let by = match p.get(key) {
None => return Err(bad(C, format!("missing `{key}`"))),
Some(v) => v
.as_str()
.and_then(VariableSortKey::parse)
.ok_or_else(|| bad(C, format!("`{key}` must be one of {}", VariableSortKey::IDS.join(", "))))?,
};
// A `descending` that is given must be true or false: null is refused, not read as omitted.
let key = "descending";
let descending = match p.get(key) {
None => false,
Some(v) => v.as_bool().ok_or_else(|| bad(C, format!("`{key}` must be true or false")))?,
};
let sort = VariableSort { by, descending };
let st = s.doc_mut()?;
if st.variables_sort != sort {
st.variables_sort = sort;
st.revision += 1;
}
Ok(json!({"by": by.id(), "descending": descending}))
}
fn dataset_new(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "dataset.new";
let name = var_name(p, C)?;
@@ -726,7 +841,15 @@ pub fn specs() -> Vec<CommandSpec> {
variables_unlocked,
rename
),
cmd!(query "variable.list", "List Variables", [], None, "{} → variables with kinds and binding counts, and whether they are locked → {variables, locked}", has_doc, list),
cmd!(
query "variable.list",
"List Variables",
[],
None,
"{} → variables with kinds and binding counts in the Variables panel's order, whether they are locked, and the panel's sort (as variable.sort sets it) → {variables, locked, sort: {by, descending}}",
has_doc,
list
),
cmd!(
"variable.makeTextDynamic",
"Make Text Dynamic",
@@ -799,6 +922,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
highlight
),
cmd!(
"variable.sort",
"Sort Variables",
[],
None,
"{by: name|object|type|none, descending?: bool} sort the Variables panel's list (a click on a column header) by variable name, by the name of the bound object, or by type (text, image, graph, visibility, then unknown); `none` is the order the variables were defined in, and `descending` reverses the order. Names compare without regard to case; by object, unbound variables come first and variables bound to several objects last, and ties go by name. Panel state: not saved and not an undo step; variable.list reports it → {by, descending}",
has_doc,
sort
),
cmd!("dataset.new", "New Data Set…", ["Window", "Variables"], None, "{name, values?: {variable: value}} add a dataset", has_doc, dataset_new),
cmd!(
"dataset.set",
+4
View File
@@ -29,6 +29,9 @@ pub struct DocState {
/// Bound Object act on. Panel state, not art selection: not saved, not undoable
/// (`variable.highlight`). A name, so renaming a variable moves the highlight with it.
pub variables_highlight: Option<String>,
/// The order of the Variables panel's list: the column it is sorted by, and whether it is
/// reversed. Panel state: not saved, not undoable (`variable.sort`).
pub variables_sort: cmd::VariableSort,
/// Isolation mode container.
pub isolation: Option<NodeId>,
/// Select › Start Global Edit: the objects edited together until Stop Global Edit or Escape
@@ -316,6 +319,7 @@ impl DocState {
active_layer,
layer_rows: vec![],
variables_highlight: None,
variables_sort: Default::default(),
isolation: None,
global_edit: None,
similar_outlines: Default::default(),
+90
View File
@@ -3382,3 +3382,93 @@ fn an_svg_without_readable_variable_data_opens_its_art() {
assert!(d.variables.variables.is_empty());
assert_eq!(vectorcraft_testkit::svg_shapes(&vectorcraft_svg::export(d, &Default::default())), 1, "the art opens");
}
/// Five variables of three kinds, defined out of every order: `beta` (text, on type named Mike),
/// `Alpha` (visibility, on a rectangle named apple), `gamma` (image, bound to nothing), `Delta`
/// (visibility, on two rectangles) and `Echo` (text, on type named Bravo).
fn sortable_variables() -> Session {
let mut s = session();
let mike = text(&mut s, "m");
let bravo = text(&mut s, "b");
let apple = rect(&mut s);
let (r1, r2) = (rect(&mut s), rect(&mut s));
for (id, name) in [(mike, "Mike"), (bravo, "Bravo"), (apple, "apple")] {
s.execute("layer.setProps", &json!({"id": id.0, "name": name})).unwrap();
}
for (name, kind, ids) in [
("beta", "text", vec![mike]),
("Alpha", "visibility", vec![apple]),
("gamma", "image", vec![]),
("Delta", "visibility", vec![r1, r2]),
("Echo", "text", vec![bravo]),
] {
s.execute("variable.define", &json!({"name": name, "kind": kind})).unwrap();
if !ids.is_empty() {
s.execute("variable.bind", &json!({"variable": name, "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
}
}
s
}
fn listed_names(s: &mut Session) -> Vec<String> {
let list = s.execute("variable.list", &json!({})).unwrap();
list["variables"].as_array().unwrap().iter().map(|v| v["name"].as_str().unwrap().to_string()).collect()
}
/// `variable.sort` orders the Variables panel's list, which `variable.list` follows, by each
/// column: names without regard to case; by object, unbound first and several objects last; by
/// type, text, image, graph, visibility. `descending` reverses each order, and `none` goes back
/// to the order the variables were defined in.
#[test]
fn variable_sort_orders_the_list_by_each_column() {
let mut s = sortable_variables();
let defined = ["beta", "Alpha", "gamma", "Delta", "Echo"];
assert_eq!(listed_names(&mut s), defined);
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["sort"], json!({"by": "none", "descending": false}));
for (by, order) in [
("name", ["Alpha", "beta", "Delta", "Echo", "gamma"]),
("object", ["gamma", "Alpha", "Echo", "beta", "Delta"]),
("type", ["beta", "Echo", "gamma", "Alpha", "Delta"]),
("none", defined),
] {
let r = s.execute("variable.sort", &json!({"by": by})).unwrap();
assert_eq!(r, json!({"by": by, "descending": false}));
assert_eq!(listed_names(&mut s), order, "{by}");
s.execute("variable.sort", &json!({"by": by, "descending": true})).unwrap();
let reversed: Vec<&str> = order.iter().rev().copied().collect();
assert_eq!(listed_names(&mut s), reversed, "{by}, descending");
assert_eq!(s.execute("variable.list", &json!({})).unwrap()["sort"], json!({"by": by, "descending": true}));
}
}
/// The sort is panel state: it is not an undo step and does not count as a change to save, and
/// setting the order the list already has does not redraw anything. A bad `by` or `descending`
/// is an error that leaves the order as it was.
#[test]
fn variable_sort_refuses_bad_params_and_is_not_an_undo_step() {
let mut s = sortable_variables();
let steps = s.doc().unwrap().history.undo.len();
let dirty = s.doc().unwrap().is_dirty();
s.execute("variable.sort", &json!({"by": "name", "descending": true})).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), steps, "no undo step");
assert_eq!(s.doc().unwrap().is_dirty(), dirty);
let revision = s.doc().unwrap().revision;
s.execute("variable.sort", &json!({"by": "name", "descending": true})).unwrap();
assert_eq!(s.doc().unwrap().revision, revision, "the same order changes nothing");
for (p, error) in [
(json!({}), "missing `by`"),
(json!({"by": null}), "`by` must be one of name, object, type, none"),
(json!({"by": "name", "descending": null}), "`descending` must be true or false"),
(json!({"by": "size"}), "`by` must be one of name, object, type, none"),
(json!({"by": "Name"}), "`by` must be one of name, object, type, none"),
(json!({"by": 1}), "`by` must be one of name, object, type, none"),
(json!({"by": "type", "descending": "yes"}), "`descending` must be true or false"),
] {
let e = s.execute("variable.sort", &p).unwrap_err().to_string();
assert!(e.contains(error), "{p}: {e}");
}
let sort = s.doc().unwrap().variables_sort;
assert_eq!((sort.by, sort.descending), (crate::cmd::VariableSortKey::Name, true), "errors leave the order alone");
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
}
+1
View File
@@ -1519,6 +1519,7 @@ font-technology Variable Променливи
Highlight anchors on mouse over Маркиране на опорните точки при задържане на мишката
Highlight Rows Маркиране на редовете
Highlight Variable Маркиране на променливи
Sort Variables Сортиране на променливите
Highlight Substituted Fonts Маркиране на заменени шрифтове
Highlight Substituted Glyphs Маркиране на заменени глифи
History История
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4216,6 +4216,7 @@ axis Both Obje
Replace: Zamijeni:
Type on a Path Options Opcije teksta na putanji
Highlight Variable Istakni varijablu
Sort Variables Sortiraj varijable
New Variable Nova varijabla
List Variables Izlistaj varijable
Bind Variable Veži varijablu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2074,6 +2074,7 @@ digits Malayalam Malaiàlam
Highlight Substituted Fonts Ressalta els tipus de lletra substituïts
Highlight Substituted Glyphs Ressalta els glifs substituïts
Highlight Variable Ressalta la variable
Sort Variables Ordena les variables
Highlight: Ressalta:
Hindi Hindi
History Historial
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4314,6 +4314,7 @@ axis Both Obě
Replace: Nahradit:
Type on a Path Options Možnosti textu na cestě
Highlight Variable Zvýraznit proměnnou
Sort Variables Seřadit proměnné
New Variable Nová proměnná
List Variables Vypsat proměnné
Bind Variable Svázat proměnnou
Can't render this file because it is too large.
+1
View File
@@ -4634,6 +4634,7 @@ axis Both Begge
Replace: Erstat:
Type on a Path Options Indstillinger for tekst på kurve
Highlight Variable Fremhæv variabel
Sort Variables Sortér variabler
New Variable Ny variabel
List Variables Oplist variabler
Bind Variable Tilknyt variabel
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5436,6 +5436,7 @@ theme Appearance Erscheinungsbild
Texture Image Strukturbild
Canvas Leinwand
Highlight Variable Variable hervorheben
Sort Variables Variablen sortieren
New Variable Neue Variable
Delete Variable Variable löschen
Lock Variables Variablen sperren
Can't render this file because it is too large.
+1
View File
@@ -1494,6 +1494,7 @@ font-technology Variable Μεταβλητές
Highlight anchors on mouse over Επισήμανση σημείων αγκύρωσης με το ποντίκι
Highlight Rows Επισήμανση γραμμών
Highlight Variable Επισήμανση μεταβλητής
Sort Variables Ταξινόμηση μεταβλητών
Highlight Substituted Fonts Επισήμανση υποκατεστημένων γραμματοσειρών
Highlight Substituted Glyphs Επισήμανση υποκατεστημένων γλύφων
History Ιστορικό
Can't render this file because it is too large.
+1
View File
@@ -1574,6 +1574,7 @@ font-technology Variable Variables
Highlight anchors on mouse over Resaltar anclas al pasar el puntero
Highlight Rows Resaltar filas
Highlight Variable Resaltar variable
Sort Variables Ordenar variables
Highlight Substituted Fonts Resaltar fuentes sustituidas
Highlight Substituted Glyphs Resaltar glifos sustituidos
History Historial
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4233,6 +4233,7 @@ axis Both Molemmat
Replace: Korvaa:
Type on a Path Options Polulla olevan tekstin asetukset
Highlight Variable Korosta muuttuja
Sort Variables Lajittele muuttujat
New Variable Uusi muuttuja
List Variables Luettele muuttujat
Bind Variable Sido muuttuja
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1568,6 +1568,7 @@ font-technology Variable Variables
Highlight anchors on mouse over Mettre en évidence les points d’ancrage au survol
Highlight Rows Surligner les lignes
Highlight Variable Surligner la variable
Sort Variables Trier les variables
Highlight Substituted Fonts Surligner les polices remplacées
Highlight Substituted Glyphs Surligner les glyphes remplacés
History Historique
Can't render this file because it is too large.
+1
View File
@@ -1679,6 +1679,7 @@
Highlight Substituted Glyphs બદલાયેલા ગ્લિફ હાઇલાઇટ કરો
Highlight Tracking ટ્રેકિંગ હાઇલાઇટ કરો
Highlight Variable વેરિએબલ હાઇલાઇટ કરો
Sort Variables વેરિએબલનો ક્રમ લગાવો
Highlight anchors on mouse over માઉસ ફેરવતાં એન્કર હાઇલાઇટ કરો
Highlight: હાઇલાઇટ:
Hindi હિન્દી
Can't render this file because it contains an unexpected character in line 72 and column 5.
+1
View File
@@ -2305,6 +2305,7 @@ antiAlias Sharp शार्प
Highlight Substituted Glyphs बदले हुए ग्लिफ़ हाइलाइट करें
Highlight Tracking हाइलाइट ट्रैकिंग
Highlight Variable वेरिएबल हाइलाइट करें
Sort Variables वेरिएबल क्रमबद्ध करें
Highlight: हाइलाइट:
Hindi हिन्दी
History हिस्ट्री
Can't render this file because it is too large.
+1
View File
@@ -1985,6 +1985,7 @@ file-filter Templates Sablonok
Highlight Substituted Fonts Helyettesített betűtípusok kiemelése
Highlight Substituted Glyphs Helyettesített glifák kiemelése
Highlight Variable Változó kiemelése
Sort Variables Változók rendezése
Hindi Hindi
History Előzmények
History Options… Előzmények beállításai…
Can't render this file because it is too large.
+1
View File
@@ -3982,6 +3982,7 @@ axis Both Keduanya
Replace: Ganti:
Type on a Path Options Opsi teks pada jalur
Highlight Variable Sorot variabel
Sort Variables Urutkan variabel
New Variable Variabel baru
List Variables Daftar variabel
Bind Variable Ikat variabel
Can't render this file because it contains an unexpected character in line 2459 and column 5.
+1
View File
@@ -1595,6 +1595,7 @@ file-filter Templates Modelli
Highlight anchors on mouse over Evidenzia ancoraggi al passaggio del mouse
Highlight Rows Evidenzia righe
Highlight Variable Evidenzia variabile
Sort Variables Ordina variabili
Highlight Substituted Fonts Evidenzia font sostituiti
Highlight Substituted Glyphs Evidenzia glifi sostituiti
History Cronologia
Can't render this file because it is too large.
+1
View File
@@ -1943,6 +1943,7 @@ antiAlias Sharp シャープ
Highlight Alternate Glyphs 代替字形をハイライト
Highlight Rows 行をハイライト
Highlight Variable 変数をハイライト表示
Sort Variables 変数を並べ替え
Highlight Substituted Fonts 置換フォントをハイライト
Highlight Substituted Glyphs 置換字形をハイライト
Highlight anchors on mouse over マウスオーバー時にアンカーをハイライト
Can't render this file because it is too large.
+1
View File
@@ -3845,6 +3845,7 @@ axis Both ორივე
Replace: ჩანაცვლება:
Type on a Path Options ტრაექტორიაზე ტექსტის პარამეტრები
Highlight Variable ცვლადის გამოკვეთა
Sort Variables ცვლადების დალაგება
New Variable ახალი ცვლადი
List Variables ცვლადების სია
Bind Variable ცვლადის მიბმა
Can't render this file because it is too large.
+1
View File
@@ -1933,6 +1933,7 @@ antiAlias Sharp 날카롭게
Highlight Alternate Glyphs 대체 글리프 강조 표시
Highlight Rows 행 강조 표시
Highlight Variable 가변 요소 강조 표시
Sort Variables 가변 요소 정렬
Highlight Substituted Fonts 대체된 글꼴 강조 표시
Highlight Substituted Glyphs 대체된 글리프 강조 표시
Highlight anchors on mouse over 마우스를 올리면 앵커 강조 표시
Can't render this file because it is too large.
+1
View File
@@ -4542,6 +4542,7 @@ axis Both Beide
Replace: Vervangen:
Type on a Path Options Opties voor tekst op pad
Highlight Variable Variabele markeren
Sort Variables Variabelen sorteren
New Variable Nieuwe variabele
List Variables Variabelen weergeven
Bind Variable Variabele binden
Can't render this file because it is too large.
+1
View File
@@ -4156,6 +4156,7 @@ axis Both Obie
Replace: Zastąp:
Type on a Path Options Opcje tekstu na ścieżce
Highlight Variable Wyróżnij zmienną
Sort Variables Sortuj zmienne
New Variable Nowa zmienna
List Variables Wyświetl zmienne
Bind Variable Powiąż zmienną
Can't render this file because it is too large.
+1
View File
@@ -3616,6 +3616,7 @@ font-technology Variable Variáveis
Graphics Gráficos
Highlight Rows Destacar linhas
Highlight Variable Destacar variável
Sort Variables Ordenar variáveis
Palette: Paleta:
Limited Limitada
Full Tone Tom completo
Can't render this file because it is too large.
+1
View File
@@ -1584,6 +1584,7 @@ file-filter Templates Шаблоны
Highlight anchors on mouse over Выделять опорные точки при наведении
Highlight Rows Выделять строки
Highlight Variable Подсветить переменную
Sort Variables Сортировать переменные
Highlight Substituted Fonts Выделять заменённые шрифты
Highlight Substituted Glyphs Выделять заменённые глифы
History История
Can't render this file because it is too large.
+1
View File
@@ -1532,6 +1532,7 @@ font-technology Variable Variabla
Highlight anchors on mouse over Framhäv ankarpunkter när musen är över dem
Highlight Rows Framhäv rader
Highlight Variable Framhäv variabel
Sort Variables Sortera variabler
Highlight Substituted Fonts Framhäv ersatta typsnitt
Highlight Substituted Glyphs Framhäv ersatta glyfer
History Historik
Can't render this file because it is too large.
+1
View File
@@ -1496,6 +1496,7 @@ font-technology Variable Değişken
Highlight anchors on mouse over Fareyle üzerine gelindiğinde bağlantı noktalarını vurgulayın
Highlight Rows Satırları Vurgula
Highlight Variable Değişkeni Vurgula
Sort Variables Değişkenleri Sırala
Highlight Substituted Fonts Değiştirilen Yazı Tiplerini Vurgula
Highlight Substituted Glyphs Değiştirilen Glifleri Vurgula
History Geçmiş
Can't render this file because it is too large.
+1
View File
@@ -1589,6 +1589,7 @@ file-filter Templates Шаблони
Highlight anchors on mouse over Виділяти опорні точки при наведенні
Highlight Rows Виділяти рядки
Highlight Variable Підсвітити змінну
Sort Variables Сортувати змінні
Highlight Substituted Fonts Виділяти замінені шрифти
Highlight Substituted Glyphs Виділяти замінені гліфи
History Історія
Can't render this file because it is too large.
+1
View File
@@ -1822,6 +1822,7 @@ file-filter Templates Mẫu
Highlight Substituted Fonts Đánh dấu phông thay thế
Highlight Substituted Glyphs Đánh dấu Glyph bị thay thế
Highlight Variable Đánh dấu biến
Sort Variables Sắp xếp biến
Hindi Tiếng Hindi
digits Hindi Hindi
History Lịch sử
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -5511,6 +5511,7 @@ font-technology Variable 可变
High Performance (discrete) 高性能 (独立显卡)
Highlight Rows 突出显示行
Highlight Variable 突出显示变量
Sort Variables 对变量排序
Lock Others 锁定其他
Merge Selected 合并所选图层
Move Rows 移动行
Can't render this file because it is too large.
+1
View File
@@ -1560,6 +1560,7 @@ file-filter Templates 範本
Highlight anchors on mouse over 滑鼠移過時醒目提示錨點
Highlight Rows 反白顯示列
Highlight Variable 突出顯示變數
Sort Variables 排序變數
Highlight Substituted Fonts 醒目提示被取代的字體
Highlight Substituted Glyphs 醒目提示被取代的字符
History 步驟記錄
Can't render this file because it is too large.
+184 -29
View File
@@ -8,17 +8,117 @@
//! the document then no longer matches the active dataset, and "Update Data Set" writes the
//! art back into it.
use egui::Ui;
use egui::{Rect, Response, Sense, Ui, pos2, vec2};
use serde_json::json;
use vectorcraft_doc::{NodeId, NodeKind, VariableKind};
use vectorcraft_engine::DocState;
use vectorcraft_engine::cmd::{VariableSort, VariableSortKey, variable_order};
use crate::VectorcraftApp;
use crate::theme::Tokens;
use crate::widgets;
/// The panel id, as the dock, the Window menu and `window.panel` name it.
pub const ID: &str = "variables";
/// The width of the variable list's type column, which holds each row's kind icon.
const TYPE_COLUMN: f32 = 22.0;
/// The width of the Variables column in a list `width` wide; the Objects column takes the rest.
fn name_column(width: f32) -> f32 {
((width - TYPE_COLUMN) / 2.0).max(0.0)
}
/// The sort a click on `key`'s header asks for: that column in ascending order, or the reverse
/// of the order the list has when it is already sorted by that column.
fn clicked_sort(sort: VariableSort, key: VariableSortKey) -> VariableSort {
VariableSort { by: key, descending: sort.by == key && !sort.descending }
}
/// The arrow a header shows while the list is sorted by its column.
fn sort_arrow(sort: VariableSort, key: VariableSortKey) -> Option<&'static str> {
(sort.by == key).then_some(if sort.descending { "▼" } else { "▲" })
}
/// Draw the list's three column headers (type, Variables, Objects); a click on one returns the
/// sort it asks for.
fn headers(ui: &mut Ui, sort: VariableSort) -> Option<VariableSort> {
let t = Tokens::get(ui.ctx());
let h = ui.spacing().interact_size.y;
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::hover());
let name_w = name_column(rect.width());
let name_left = rect.left() + TYPE_COLUMN;
let cells = [
(VariableSortKey::Type, tl!("Type"), Rect::from_min_max(rect.min, pos2(name_left, rect.bottom()))),
(VariableSortKey::Name, tl!("Variables"), Rect::from_min_max(pos2(name_left, rect.top()), pos2(name_left + name_w, rect.bottom()))),
(VariableSortKey::Object, tl!("Objects"), Rect::from_min_max(pos2(name_left + name_w, rect.top()), rect.max)),
];
let font = egui::TextStyle::Body.resolve(ui.style());
let mut clicked = None;
for (key, label, cell) in cells {
let r = ui.interact(cell, ui.scope_id().with(("variables-sort", key.id())), Sense::click());
if r.hovered() {
ui.painter().rect_filled(cell, 0.0, t.hover);
}
let arrow = sort_arrow(sort, key);
let p = ui.painter().with_clip_rect(cell);
if key == VariableSortKey::Type {
// The type column is too narrow for a word: its icon, or the arrow once it sorts.
match arrow {
Some(a) => {
p.text(cell.center(), egui::Align2::CENTER_CENTER, a, font.clone(), t.text_dim);
}
None => crate::icons::paint(ui, "arrow-up-down", Rect::from_center_size(cell.center(), vec2(14.0, 14.0)), t.icon),
}
} else {
p.text(cell.left_center() + vec2(4.0, 0.0), egui::Align2::LEFT_CENTER, label, font.clone(), t.text_strong);
if let Some(a) = arrow {
p.text(cell.right_center() - vec2(4.0, 0.0), egui::Align2::RIGHT_CENTER, a, font.clone(), t.text_dim);
}
}
r.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, arrow.is_some(), label));
if r.on_hover_text(label).clicked() {
clicked = Some(clicked_sort(sort, key));
}
}
clicked
}
/// Draw one variable row across the three columns: its kind's icon, its name and the object it
/// drives. Each column clips its text to its own width.
fn variable_row(ui: &mut Ui, row: &VarRow, selected: bool) -> Response {
let t = Tokens::get(ui.ctx());
let h = ui.spacing().interact_size.y;
let (rect, r) = ui.allocate_exact_size(vec2(ui.available_width(), h), Sense::click());
if selected {
ui.painter().rect_filled(rect, 0.0, t.row_selected);
} else if r.hovered() {
ui.painter().rect_filled(rect, 0.0, t.hover);
}
// The kind's own icon, so a text variable and a visibility one read apart.
let icon = match row.kind {
VariableKind::Text => "type",
VariableKind::Visibility => "eye",
VariableKind::Image => "image",
VariableKind::Graph => "chart-column",
VariableKind::Unknown => "square-dashed",
};
let icon_rect = Rect::from_center_size(pos2(rect.left() + TYPE_COLUMN / 2.0, rect.center().y), vec2(14.0, 14.0));
crate::icons::paint(ui, icon, icon_rect, t.icon);
let name_left = rect.left() + TYPE_COLUMN;
let name_right = name_left + name_column(rect.width());
let font = egui::TextStyle::Body.resolve(ui.style());
let (pad, left) = (vec2(4.0, 0.0), egui::Align2::LEFT_CENTER);
let name_cell = Rect::from_min_max(pos2(name_left, rect.top()), pos2(name_right - 4.0, rect.bottom()));
ui.painter().with_clip_rect(name_cell).text(name_cell.left_center() + pad, left, &row.name, font.clone(), t.text);
let object_cell = Rect::from_min_max(pos2(name_right, rect.top()), rect.max);
ui.painter().with_clip_rect(object_cell).text(object_cell.left_center() + pad, left, &row.object, font, t.text_dim);
let full = if row.object.is_empty() { row.name.clone() } else { format!("{} · {}", row.name, row.object) };
r.widget_info(|| egui::WidgetInfo::selected(egui::Role::ListItem, true, selected, &full));
// A column clips a long name, so the whole of it shows on hover.
r.on_hover_text(full)
}
/// One variable row: its kind's icon, its name, and the object it drives.
struct VarRow {
name: String,
@@ -105,11 +205,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
};
// Owned snapshots: the rows below run commands, which borrow the app mutably.
let xml_id = app.session.prefs.identify_objects_by == "xmlId";
let vars: Vec<VarRow> = st
.doc
.variables
.variables
.iter()
// In the order `variable.sort` set, which `variable.list` reports too.
let all = &st.doc.variables.variables;
let vars: Vec<VarRow> = variable_order(st, xml_id)
.into_iter()
.filter_map(|i| all.get(i))
.map(|v| {
let bound: Vec<u64> = st.doc.variables.objects_of(&v.name).iter().map(|i| i.0).collect();
let object = match bound.as_slice() {
@@ -139,6 +239,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let highlighted = highlighted_row(st);
// Lock Variables: no variable is defined, deleted or renamed until they are unlocked.
let locked = st.doc.variables.locked;
let sort = st.variables_sort;
// The current data set across the top: its name, and the arrows that step through them.
ui.horizontal(|ui| {
@@ -162,33 +263,24 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
});
});
widgets::subheader(ui, tl!("Variables"));
// The list's column headers: a click sorts the list by that column, and a second click
// reverses the order.
if let Some(next) = headers(ui, sort) {
app.run("variable.sort", json!({"by": next.by.id(), "descending": next.descending})).ok();
}
if vars.is_empty() {
widgets::dim_label(ui, tl!("None"));
}
for row in &vars {
ui.horizontal(|ui| {
// The kind's own icon, so a text variable and a visibility one read apart.
let icon = match row.kind {
VariableKind::Text => "type",
VariableKind::Visibility => "eye",
VariableKind::Image => "image",
VariableKind::Graph => "chart-column",
VariableKind::Unknown => "square-dashed",
};
ui.add(egui::Image::new(crate::icons::source(icon)).fit_to_exact_size(egui::vec2(14.0, 14.0)));
// The object a variable drives is named beside it, as the reference panel does.
let text = if row.object.is_empty() { row.name.clone() } else { format!("{} · {}", row.name, row.object) };
let r = ui.selectable_label(highlighted.as_deref() == Some(row.name.as_str()), text);
// A click highlights the row and selects what it drives; a double click opens
// Variable Options for it.
if r.clicked() {
click_row(app, &row.name, &row.bound);
}
if r.double_clicked() && !locked {
crate::dialogs::variables::open(app, "variable.rename", Some(&row.name));
}
});
let r = variable_row(ui, row, highlighted.as_deref() == Some(row.name.as_str()));
// A click highlights the row and selects what it drives; a double click opens
// Variable Options for it.
if r.clicked() {
click_row(app, &row.name, &row.bound);
}
if r.double_clicked() && !locked {
crate::dialogs::variables::open(app, "variable.rename", Some(&row.name));
}
}
widgets::subheader(ui, tl!("Data Sets"));
@@ -406,6 +498,69 @@ mod tests {
assert!(text.contains("Name") && text.contains("Data Sets") && text.contains("Data Set 1"), "{text}");
}
/// A click on a column header sorts the list by that column, and a second click on it
/// reverses the order; the rows are drawn in the order `variable.list` reports.
#[test]
fn a_header_click_sorts_the_list_and_a_second_click_reverses_it() {
use super::super::tests_appearance::{click, frame_events, text_rect};
let mut app = app_with_data();
let t = app.session.execute("text.create", &json!({"x": 10, "y": 80, "text": "Bob"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("layer.setProps", &json!({"id": t, "name": "Aaron"})).unwrap();
let alice = app.session.doc().unwrap().doc.variables.objects_of("Name")[0];
app.session.execute("layer.setProps", &json!({"id": alice.0, "name": "Bella"})).unwrap();
app.session.execute("variable.define", &json!({"name": "age", "kind": "visibility"})).unwrap();
app.session.execute("variable.bind", &json!({"variable": "age", "ids": [t]})).unwrap();
let ctx = egui::Context::default();
let drawn = |ctx: &egui::Context, app: &mut VectorcraftApp| -> Vec<String> {
let texts = frame_events(ctx, app, vec![], show);
let mut rows: Vec<(f32, String)> = ["Name", "age"].iter().map(|n| (text_rect(&texts, n).center().y, n.to_string())).collect();
rows.sort_by(|a, b| a.0.total_cmp(&b.0));
rows.into_iter().map(|(_, n)| n).collect()
};
let listed = |app: &mut VectorcraftApp| -> Vec<String> {
let list = app.run("variable.list", json!({})).unwrap();
list["variables"].as_array().unwrap().iter().map(|v| v["name"].as_str().unwrap().to_string()).collect()
};
let sort = |app: &VectorcraftApp| {
let s = app.session.doc().unwrap().variables_sort;
(s.by, s.descending)
};
assert_eq!(drawn(&ctx, &mut app), ["Name", "age"], "the order they were defined in");
let steps = app.session.doc().unwrap().history.undo.len();
let header = |ctx: &egui::Context, app: &mut VectorcraftApp, label: &str| text_rect(&frame_events(ctx, app, vec![], show), label).center();
let at = header(&ctx, &mut app, "Variables");
click(&ctx, &mut app, at, show);
assert_eq!(sort(&app), (VariableSortKey::Name, false));
assert_eq!(drawn(&ctx, &mut app), ["age", "Name"]);
assert_eq!(listed(&mut app), ["age", "Name"]);
let at = header(&ctx, &mut app, "Variables");
click(&ctx, &mut app, at, show);
assert_eq!(sort(&app), (VariableSortKey::Name, true));
assert_eq!(drawn(&ctx, &mut app), ["Name", "age"]);
// Objects: Aaron (age's type) before Bella (Name's), then the reverse.
let at = header(&ctx, &mut app, "Objects");
click(&ctx, &mut app, at, show);
assert_eq!(sort(&app), (VariableSortKey::Object, false));
assert_eq!(drawn(&ctx, &mut app), ["age", "Name"]);
let at = header(&ctx, &mut app, "Objects");
click(&ctx, &mut app, at, show);
assert_eq!(drawn(&ctx, &mut app), ["Name", "age"]);
// The type column's header sits left of the Variables header: text before visibility.
let left = text_rect(&frame_events(&ctx, &mut app, vec![], show), "Variables").left() - 4.0 - TYPE_COLUMN / 2.0;
let y = header(&ctx, &mut app, "Variables").y;
click(&ctx, &mut app, egui::pos2(left, y), show);
assert_eq!(sort(&app), (VariableSortKey::Type, false));
assert_eq!(drawn(&ctx, &mut app), ["Name", "age"]);
click(&ctx, &mut app, egui::pos2(left, y), show);
assert_eq!(sort(&app), (VariableSortKey::Type, true));
assert_eq!(drawn(&ctx, &mut app), ["age", "Name"]);
// Sorting is not an edit.
assert_eq!(app.session.doc().unwrap().history.undo.len(), steps, "no undo step");
}
#[test]
fn a_row_the_art_left_behind_is_in_italics() {
let mut app = app_with_data();
+8 -1
View File
@@ -3418,7 +3418,14 @@ already walks Image Trace. Short agent entry points:
without a kind VectorCraft reads is listed with `kind: "unknown"`: it takes no values, and `variable.bind`
refuses it. When an opened `.ai`'s layers are read, the objects it binds to its text, visibility and image
variables come in bound when they are of the variable's kind, locked and hidden objects included, and
`variable.list` counts them in `bindings`.
`variable.list` counts them in `bindings`. `variable.sort {by: name|object|type|none, descending?}` sorts the
Variables panel's list, as a click on its Variables, Objects or type column header does (a second click on the
same header sends `descending: true`): by variable name, by the name of the bound object, or by type in
VectorCraft's order (text, image, graph, visibility, then unknown); `none` is the order the variables were
defined in. Names compare without regard to case; by object, unbound variables come first and variables bound
to several objects last, and ties go by name. Another `by` value, or a `descending` that is not true or false,
is an error. The sort is panel state, not saved and not an undo step; `variable.list` lists the variables in
that order and reports it as `sort: {by, descending}`.
- Data sets: `dataset.list`, `dataset.new {name, values?}`, `dataset.set {name, newName?, values?}` (`newName` also
renames the row, in the same undo step; Edit Data Set's Name field sends it), `dataset.rename`,
`dataset.delete`, `dataset.capture`, `dataset.update`, `dataset.select {name}`, `dataset.next` /
+1
View File
@@ -788,6 +788,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- 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: when no object is bound to a variable that the data set has a value for, for example after Import Data Sets… defines new variables without binding them, choosing the data set in the Variables panel or stepping to it with the panel's arrows shows a status message, and `dataset.select`, `dataset.next` and `dataset.prev` add the message to the reply as `warning`.
- 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.
- Variables: the Variables panel lists its variables in three columns with headers (type, Variables, Objects), and a click on a header sorts the list by variable type, variable name or bound object name, with a second click reversing the order (`variable.sort`; `variable.list` follows the order), as the [Variable panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/variable-panel-overview.html) describes.
- Batch…: Override Save and Override Export name each file with the extension of the format the step writes, however the step spells the format (`.svg` and `SVG` give `.svg`, `png8` gives `.png`), in `action.batch`.
- Batch…: Override Export writes the files of an Export for Screens or Export Assets step into a folder for each source file or data set inside the destination folder, under the names the export gives them (`action.batch`). The folder and file names drop trailing periods and spaces (`a..svg` gives `a`). When two source files would get the same name that way (compared without case), or a later file's folder or file would take the name of an output an earlier file wrote, of a source file in the destination folder or, for data sets, of a file already there, Batch adds `-2`, `-3`… to the later one's name.
- Batch…: when a step fails on a file or data set, the error names the step by its place in the action's steps, from 1, counting the Open steps the batch skips (`action.batch`). This numbering is VectorCraft's own rule. Tests `cmd::fileio::action_batch::tests::{a_folder_batch_error_names_the_step_in_the_commands_sent, a_recorded_open_that_fails_is_the_first_step, a_data_set_batch_error_names_the_step_in_the_commands_sent, a_redirected_export_that_fails_keeps_its_place, a_source_that_does_not_open_names_no_step}`.