Merge pull request #1809 from Messhias/feat/394-identify-objects-by
Part of #394: Identify Objects By (Object Name / XML ID)
This commit is contained in:
commit
a23f5711af
|
|
@ -48,6 +48,7 @@ pub use symbols::SymbolBoxes;
|
|||
pub mod text;
|
||||
pub mod trace;
|
||||
pub mod variables;
|
||||
pub mod xml_id;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
|
|
|||
|
|
@ -978,6 +978,12 @@ impl Node {
|
|||
_ => format!("<{}>", self.kind_label()),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::display_name`], or its XML id when Preferences › Units › Identify Objects By is XML ID.
|
||||
pub fn identify_label(&self, xml_id: bool) -> String {
|
||||
let name = self.display_name();
|
||||
if xml_id { crate::xml_id::from_object_name(&name) } else { name }
|
||||
}
|
||||
/// Path data for path nodes.
|
||||
pub fn path_data(&self) -> Option<&PathData> {
|
||||
match &self.kind {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
//! Object names ↔ XML IDs (Preferences › Units › Identify Objects By).
|
||||
//!
|
||||
//! Illustrator's public documentation: with XML ID selected, the Layers panel shows and edits
|
||||
//! names that conform to XML naming (letter, underscore or colon first; no spaces). Underscores
|
||||
//! stand for spaces and `_xHH_` / `_xHHHH_` escape other characters, as in `.ai` `AI10_ArtUID`
|
||||
//! values ([edit dynamic objects](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/edit-dynamic-objects.html)).
|
||||
|
||||
/// An object name from its XML id: `_` is a space and `_xHH_` / `_xHHHH_` a character; ids made
|
||||
/// unique end with `_` and 20 or more digits (stripped).
|
||||
pub fn to_object_name(id: &str) -> String {
|
||||
let id = strip_unique(id);
|
||||
let mut out = String::with_capacity(id.len());
|
||||
let mut rest = id;
|
||||
while let Some(c) = rest.chars().next() {
|
||||
if c == '_' {
|
||||
if let Some((ch, n)) = escaped(rest) {
|
||||
out.push(ch);
|
||||
rest = rest.get(n..).unwrap_or_default();
|
||||
continue;
|
||||
}
|
||||
out.push(' ');
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
rest = rest.get(c.len_utf8()..).unwrap_or_default();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The XML id for an object name (the inverse of [`to_object_name`]).
|
||||
pub fn from_object_name(name: &str) -> String {
|
||||
let mut out = String::with_capacity(name.len().saturating_mul(2));
|
||||
for (i, c) in name.chars().enumerate() {
|
||||
if c == ' ' {
|
||||
out.push('_');
|
||||
} else if c == '_' {
|
||||
out.push_str("_x5F_");
|
||||
} else if i == 0 && c.is_ascii_digit() {
|
||||
out.push_str(&format!("_x{:X}_", u32::from(c)));
|
||||
} else if c.is_alphanumeric() || c == '.' || c == '-' || c == ':' {
|
||||
out.push(c);
|
||||
} else {
|
||||
out.push_str(&format!("_x{:X}_", u32::from(c)));
|
||||
}
|
||||
}
|
||||
if out.is_empty() || !starts_name(&out) {
|
||||
out.insert(0, '_');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether `id` is a usable XML ID for Identify Objects By › XML ID: non-empty, no whitespace,
|
||||
/// starts with a letter, `_` or `:`, and otherwise letters, digits, `.`, `-`, `_` or `:`.
|
||||
pub fn is_valid(id: &str) -> bool {
|
||||
let id = id.trim();
|
||||
if id.is_empty() || id.chars().any(char::is_whitespace) {
|
||||
return false;
|
||||
}
|
||||
let mut chars = id.chars();
|
||||
let Some(first) = chars.next() else {
|
||||
return false;
|
||||
};
|
||||
if !(first.is_alphabetic() || first == '_' || first == ':') {
|
||||
return false;
|
||||
}
|
||||
chars.all(|c| c.is_alphanumeric() || matches!(c, '_' | '.' | '-' | ':'))
|
||||
}
|
||||
|
||||
fn starts_name(id: &str) -> bool {
|
||||
id.starts_with(|c: char| c.is_alphabetic() || c == '_' || c == ':')
|
||||
}
|
||||
|
||||
/// `_xHHHH_` at the start of `s`: the character and the escape's length.
|
||||
fn escaped(s: &str) -> Option<(char, usize)> {
|
||||
let body = s.strip_prefix("_x")?;
|
||||
let end = body.find('_')?;
|
||||
let hex = body.get(..end)?;
|
||||
if hex.is_empty() || hex.len() > 6 {
|
||||
return None;
|
||||
}
|
||||
let c = char::from_u32(u32::from_str_radix(hex, 16).ok()?)?;
|
||||
Some((c, end + 3))
|
||||
}
|
||||
|
||||
/// `id` without the `_000…_` suffix that makes it unique.
|
||||
fn strip_unique(id: &str) -> &str {
|
||||
let Some(body) = id.strip_suffix('_') else {
|
||||
return id;
|
||||
};
|
||||
let Some(at) = body.rfind('_') else {
|
||||
return id;
|
||||
};
|
||||
let digits = body.get(at + 1..).unwrap_or_default();
|
||||
if digits.len() >= 20 && digits.bytes().all(|b| b.is_ascii_digit()) { body.get(..at).unwrap_or(id) } else { id }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip_ai_style_ids() {
|
||||
for (name, id) in [
|
||||
("Layer 1", "Layer_1"),
|
||||
("My Rect #2", "My_Rect__x23_2"),
|
||||
("1abc (paren)", "_x31_abc__x28_paren_x29_"),
|
||||
("Ünï 日本", "Ünï_日本"),
|
||||
("a_b", "a_x5F_b"),
|
||||
] {
|
||||
assert_eq!(from_object_name(name), id, "encode {name}");
|
||||
assert_eq!(to_object_name(id), name, "decode {id}");
|
||||
}
|
||||
assert_eq!(to_object_name("S_00000092429200007574234970000005974767392385616558_"), "S");
|
||||
assert!(is_valid("Layer_1") && is_valid("_x31_abc") && is_valid(":root"));
|
||||
assert!(!is_valid("My Rect") && !is_valid("1abc") && !is_valid(""));
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"Layer Options…",
|
||||
["Window", "Layers"],
|
||||
None,
|
||||
"{ids?|id? (default: the highlighted rows, else the current layer), name?, visible?, locked?, template?, printable?, preview?, dimImages?: 0..100|false, color?: index 0..26|\"#rrggbb\"|preset name} Layer Options; rows that are not layers take name, visible and locked. Template on also locks the layer and dims its images to 50% (unless given); off unlocks it and stops dimming. One undo step",
|
||||
"{ids?|id? (default: the highlighted rows, else the current layer), name? (with prefs.identifyObjectsBy xmlId: a valid XML ID, stored as the object name), visible?, locked?, template?, printable?, preview?, dimImages?: 0..100|false, color?: index 0..26|\"#rrggbb\"|preset name} Layer Options; rows that are not layers take name, visible and locked. Template on also locks the layer and dims its images to 50% (unless given); off unlocks it and stops dimming. One undo step",
|
||||
has_doc,
|
||||
set_props
|
||||
),
|
||||
|
|
@ -297,7 +297,9 @@ fn layer_color_param(v: &Value) -> std::result::Result<LayerColor, String> {
|
|||
}
|
||||
|
||||
/// Layer Options in `p` applied to layer or row `n` (validated first by [`check_options`]).
|
||||
fn apply_options(n: &mut Node, p: &Value) {
|
||||
/// `xml_id`: Preferences › Units › Identify Objects By › XML ID — `name` is an XML id and is
|
||||
/// stored as the object name ([`vectorcraft_doc::xml_id`]).
|
||||
fn apply_options(n: &mut Node, p: &Value, xml_id: bool) {
|
||||
if let Some(v) = str_param(p, "name") {
|
||||
if v.trim().is_empty() {
|
||||
// A layer keeps a name; an object goes back to its generated `<Kind>` one.
|
||||
|
|
@ -305,7 +307,8 @@ fn apply_options(n: &mut Node, p: &Value) {
|
|||
n.name = None;
|
||||
}
|
||||
} else {
|
||||
n.name = Some(v.chars().take(512).collect());
|
||||
let name = if xml_id { vectorcraft_doc::xml_id::to_object_name(v) } else { v.to_string() };
|
||||
n.name = Some(name.chars().take(512).collect());
|
||||
}
|
||||
}
|
||||
if let Some(v) = p.get("visible").and_then(Value::as_bool) {
|
||||
|
|
@ -354,7 +357,9 @@ fn apply_options(n: &mut Node, p: &Value) {
|
|||
}
|
||||
|
||||
/// Bad values in Layer Options params, as an error for `cmd`.
|
||||
fn check_options(cmd: &str, p: &Value) -> Result<()> {
|
||||
/// `xml_id`: Preferences › Units › Identify Objects By › XML ID — a non-empty `name` must be a
|
||||
/// valid XML id.
|
||||
fn check_options(cmd: &str, p: &Value, xml_id: bool) -> Result<()> {
|
||||
if let Some(v) = p.get("color") {
|
||||
layer_color_param(v).map_err(|e| bad(cmd, e))?;
|
||||
}
|
||||
|
|
@ -368,6 +373,12 @@ fn check_options(cmd: &str, p: &Value) -> Result<()> {
|
|||
{
|
||||
return Err(bad(cmd, "`dimImages` is a percentage or false"));
|
||||
}
|
||||
if xml_id
|
||||
&& let Some(name) = str_param(p, "name").filter(|n| !n.trim().is_empty())
|
||||
&& !vectorcraft_doc::xml_id::is_valid(name)
|
||||
{
|
||||
return Err(bad(cmd, "Identify Objects By is XML ID: the name must be a valid XML ID (start with a letter, underscore or colon; no spaces)"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -446,12 +457,13 @@ fn clip_toggle(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
/// A new layer with the options in `p`, at `parent`/`index`; it becomes current and highlighted.
|
||||
fn add_layer(s: &mut Session, p: &Value, cmd: &str, label: &str, place: impl FnOnce(&Document) -> Result<(Option<NodeId>, usize)>) -> Result<Value> {
|
||||
check_options(cmd, p)?;
|
||||
let xml_id = s.prefs.identify_objects_by == "xmlId";
|
||||
check_options(cmd, p, xml_id)?;
|
||||
let name = str_param(p, "name").filter(|n| !n.trim().is_empty()).map(str::to_string);
|
||||
let id = s.edit(label, |d, _| {
|
||||
let (parent, index) = place(d)?;
|
||||
let mut n = make_layer(d, name.as_deref());
|
||||
apply_options(&mut n, p);
|
||||
apply_options(&mut n, p, xml_id);
|
||||
Ok(d.insert(parent, index, n)?)
|
||||
})?;
|
||||
let st = s.doc_mut()?;
|
||||
|
|
@ -568,14 +580,15 @@ fn highlight(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
|
||||
fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "layer.setProps";
|
||||
check_options(C, p)?;
|
||||
let xml_id = s.prefs.identify_objects_by == "xmlId";
|
||||
check_options(C, p, xml_id)?;
|
||||
let ids = rows_param(s, p, C)?;
|
||||
if ids.is_empty() {
|
||||
return Err(bad(C, "no rows"));
|
||||
}
|
||||
s.edit("Layer Options", |d, sel| {
|
||||
for id in &ids {
|
||||
apply_options(d.node_mut(*id).ok_or(EngineError::NoNode(*id))?, p);
|
||||
apply_options(d.node_mut(*id).ok_or(EngineError::NoNode(*id))?, p, xml_id);
|
||||
}
|
||||
sel.deselect_uneditable(d);
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -238,6 +238,25 @@ fn visibility_and_lock_per_row_are_undoable() {
|
|||
assert_eq!(node(&s, sub).display_name(), "Sub", "a layer keeps its name");
|
||||
}
|
||||
|
||||
/// Preferences › Units › Identify Objects By › XML ID (#394): `layer.setProps` takes a valid XML
|
||||
/// id and stores the object name; Object Name (the default) keeps free text. The Layers label is
|
||||
/// the XML id when that preference is on.
|
||||
#[test]
|
||||
fn identify_objects_by_xml_id_names_layers() {
|
||||
let mut s = session();
|
||||
let a = rect(&mut s, 0.0);
|
||||
run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My Rect #2"}));
|
||||
assert_eq!(node(&s, a).display_name(), "My Rect #2");
|
||||
assert_eq!(node(&s, a).identify_label(true), "My_Rect__x23_2");
|
||||
run(&mut s, "prefs.set", json!({"key": "identifyObjectsBy", "value": "xmlId"}));
|
||||
assert!(s.execute("layer.setProps", &json!({"id": a.0, "name": "My Rect"})).is_err(), "spaces are refused");
|
||||
run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My_Rect__x23_2"}));
|
||||
assert_eq!(node(&s, a).display_name(), "My Rect #2", "the XML id is stored as the object name");
|
||||
run(&mut s, "prefs.set", json!({"key": "identifyObjectsBy", "value": "objectName"}));
|
||||
run(&mut s, "layer.setProps", json!({"id": a.0, "name": "My Rect"}));
|
||||
assert_eq!(node(&s, a).display_name(), "My Rect");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_set_props_deselects_the_objects_it_hides_or_locks() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
|
|
@ -123,46 +123,9 @@ pub(super) fn close(o: &mut Obj, stack: &mut Vec<V>, base: usize) {
|
|||
}
|
||||
}
|
||||
|
||||
/// An object name from its XML id (`AI10_ArtUID`): `_` stands for a space and `_xHH_` for a
|
||||
/// character; ids made unique end with `_` and 20 or more digits.
|
||||
/// An object name from its XML id (`AI10_ArtUID`).
|
||||
pub(super) fn xml_name(id: &str) -> String {
|
||||
let id = strip_unique(id);
|
||||
let mut out = String::with_capacity(id.len());
|
||||
let mut rest = id;
|
||||
while let Some(c) = rest.chars().next() {
|
||||
if c == '_' {
|
||||
if let Some((ch, n)) = escaped(rest) {
|
||||
out.push(ch);
|
||||
rest = rest.get(n..).unwrap_or_default();
|
||||
continue;
|
||||
}
|
||||
out.push(' ');
|
||||
} else {
|
||||
out.push(c);
|
||||
}
|
||||
rest = rest.get(c.len_utf8()..).unwrap_or_default();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `_xHHHH_` at the start of `s`: the character and the escape's length.
|
||||
fn escaped(s: &str) -> Option<(char, usize)> {
|
||||
let body = s.strip_prefix("_x")?;
|
||||
let end = body.find('_')?;
|
||||
let hex = body.get(..end)?;
|
||||
if hex.is_empty() || hex.len() > 6 {
|
||||
return None;
|
||||
}
|
||||
let c = char::from_u32(u32::from_str_radix(hex, 16).ok()?)?;
|
||||
Some((c, end + 3))
|
||||
}
|
||||
|
||||
/// `id` without the `_000…_` suffix that makes it unique.
|
||||
fn strip_unique(id: &str) -> &str {
|
||||
let Some(body) = id.strip_suffix('_') else { return id };
|
||||
let Some(at) = body.rfind('_') else { return id };
|
||||
let digits = body.get(at + 1..).unwrap_or_default();
|
||||
if digits.len() >= 20 && digits.bytes().all(|b| b.is_ascii_digit()) { body.get(..at).unwrap_or(id) } else { id }
|
||||
vectorcraft_doc::xml_id::to_object_name(id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@ fn color_field(c: LayerColor) -> String {
|
|||
}
|
||||
|
||||
/// The fields for `n` (a layer or any other row).
|
||||
fn fields_of(n: &Node) -> serde_json::Map<String, Value> {
|
||||
/// `xml_id`: Preferences › Units › Identify Objects By › XML ID.
|
||||
fn fields_of(n: &Node, xml_id: bool) -> serde_json::Map<String, Value> {
|
||||
let mut f = serde_json::Map::new();
|
||||
f.insert("name".into(), json!(n.display_name()));
|
||||
f.insert("name".into(), json!(n.identify_label(xml_id)));
|
||||
f.insert("visible".into(), json!(n.visible));
|
||||
f.insert("locked".into(), json!(n.locked));
|
||||
f.insert("layer".into(), json!(n.is_layer()));
|
||||
|
|
@ -70,7 +71,8 @@ pub fn open(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
|||
}
|
||||
};
|
||||
let first = ids.first().and_then(|id| st.doc.node(*id)).ok_or("no such layer or object")?;
|
||||
let mut f = fields_of(first);
|
||||
let xml_id = app.session.prefs.identify_objects_by == "xmlId";
|
||||
let mut f = fields_of(first, xml_id);
|
||||
f.insert("ids".into(), json!(ids.iter().map(|i| i.0).collect::<Vec<_>>()));
|
||||
f.insert("mode".into(), json!("edit"));
|
||||
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields: f });
|
||||
|
|
@ -93,8 +95,10 @@ pub fn open_new(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
|||
}
|
||||
let sublayer = p.get("sublayer").and_then(Value::as_bool).unwrap_or(false);
|
||||
let color = LayerColor::Preset((layers.len() % LAYER_COLORS.len()) as u8);
|
||||
let name = format!("Layer {i}");
|
||||
let name = if app.session.prefs.identify_objects_by == "xmlId" { vectorcraft_doc::xml_id::from_object_name(&name) } else { name };
|
||||
let fields = json!({
|
||||
"mode": if sublayer { "newSublayer" } else { "new" }, "layer": true, "name": format!("Layer {i}"), "color": color_field(color),
|
||||
"mode": if sublayer { "newSublayer" } else { "new" }, "layer": true, "name": name, "color": color_field(color),
|
||||
"template": false, "locked": false, "visible": true, "printable": true, "preview": true, "dimImages": false, "dimPercent": 50,
|
||||
});
|
||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
||||
|
|
|
|||
|
|
@ -169,7 +169,8 @@ fn drag_ghost(ui: &Ui, view: &View, src: NodeId, pos: egui::Pos2) {
|
|||
if view.opts.thumb(n) {
|
||||
x = row_thumb(&ui, view, n, r, x).right() + 4.0;
|
||||
}
|
||||
let name = painted_name(n, &n.display_name(), crate::i18n::current());
|
||||
let label = n.identify_label(view.identify_xml_id);
|
||||
let name = if view.identify_xml_id { label } else { painted_name(n, &label, crate::i18n::current()) };
|
||||
ui.painter().text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(13.0), t.text);
|
||||
}
|
||||
|
||||
|
|
@ -329,6 +330,8 @@ struct View<'a> {
|
|||
column: Option<ColumnDrag>,
|
||||
/// The row to scroll into view.
|
||||
reveal: Option<NodeId>,
|
||||
/// Preferences › Units › Identify Objects By › XML ID.
|
||||
identify_xml_id: bool,
|
||||
}
|
||||
|
||||
/// What one frame of the panel decided.
|
||||
|
|
@ -390,6 +393,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
h: app.ui.layers_panel.row(),
|
||||
column: ui.data(|d| d.get_temp(key("column"))),
|
||||
reveal,
|
||||
identify_xml_id: app.session.prefs.identify_objects_by == "xmlId",
|
||||
};
|
||||
let mut out = Out::default();
|
||||
ui.add_space(6.0);
|
||||
|
|
@ -685,8 +689,9 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
|||
x = th.right() + 4.0;
|
||||
th
|
||||
});
|
||||
// Name.
|
||||
let name = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { "<Opacity Mask>".to_string() } else { n.display_name() };
|
||||
// Name (Preferences › Units › Identify Objects By: Object Name or XML ID).
|
||||
let xml_id = view.identify_xml_id;
|
||||
let name = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { "<Opacity Mask>".to_string() } else { n.identify_label(xml_id) };
|
||||
let font = egui::FontId::proportional(13.0);
|
||||
let renaming: Option<(u64, String)> = ui.data(|d| d.get_temp(rename_id()));
|
||||
let name_rect = egui::Rect::from_min_max(egui::pos2(x - 2.0, r.center().y - 10.0), egui::pos2(r.right() - 44.0, r.center().y + 10.0));
|
||||
|
|
@ -713,10 +718,13 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
|||
_ => {
|
||||
let painter = ui.painter().with_clip_rect(name_rect);
|
||||
// Generated names ("<Path>", "<Opacity Mask>") are translated where painted; the stored name stays English.
|
||||
// XML IDs stay as ids (not translated).
|
||||
let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) {
|
||||
tl!("<Opacity Mask>").to_string()
|
||||
} else if xml_id {
|
||||
name.clone()
|
||||
} else {
|
||||
painted_name(n, &name, crate::i18n::current())
|
||||
painted_name(n, &n.display_name(), crate::i18n::current())
|
||||
};
|
||||
let text = painter.text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, shown, font, if item.dim { t.text_dim } else { t.text });
|
||||
name_end = text.right();
|
||||
|
|
@ -758,7 +766,7 @@ fn row(ui: &mut Ui, view: &View, item: &Row, out: &mut Out) {
|
|||
let on_name = ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| p.x <= name_end.max(name_rect.left() + 20.0) && p.x >= name_rect.left());
|
||||
let on_thumb = thumb_rect.is_some_and(|th| ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| th.contains(p)));
|
||||
if on_name && !on_thumb {
|
||||
ui.data_mut(|d| d.insert_temp(rename_id(), (n.id.0, n.display_name())));
|
||||
ui.data_mut(|d| d.insert_temp(rename_id(), (n.id.0, n.identify_label(view.identify_xml_id))));
|
||||
} else {
|
||||
out.actions.push(("ui.layerOptions".into(), json!({"ids": [n.id.0]})));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
return;
|
||||
};
|
||||
// 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
|
||||
|
|
@ -106,7 +107,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let bound: Vec<u64> = st.doc.variables.objects_of(&v.name).iter().map(|i| i.0).collect();
|
||||
let object = match bound.as_slice() {
|
||||
[] => String::new(),
|
||||
[one] => st.doc.node(NodeId(*one)).map(|n| n.display_name()).unwrap_or_default(),
|
||||
[one] => st.doc.node(NodeId(*one)).map(|n| n.identify_label(xml_id)).unwrap_or_default(),
|
||||
_ => tl!("Multiple objects").to_string(),
|
||||
};
|
||||
VarRow { name: v.name.clone(), kind: v.kind, bound, object }
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1991,7 +1991,10 @@ three: General (rulers, positions and sizes, the Info panel, dialog distances, c
|
|||
`numbersWithoutUnitsArePoints` (on by default) makes the desktop app's length fields in picas read a number typed
|
||||
with no unit in points (`12` is 12 pt, `2p6` and `1p` stay picas, `12 mm` millimetres); off, it is in picas. Fields in
|
||||
other units read it in their unit either way, and the Preferences dialog dims the option unless a unit is Picas.
|
||||
Command params are in points either way.
|
||||
Command params are in points either way. `identifyObjectsBy` (`objectName` by default, or `xmlId`) sets how Layers,
|
||||
Variables and Layer Options show and edit object names: Object Name keeps free text; XML ID shows AI-style XML ids
|
||||
(`My Rect #2` as `My_Rect__x23_2`) and `layer.setProps` / rename require a valid XML ID (letter, underscore or colon
|
||||
first; no spaces), stored back as the object name.
|
||||
`prefs.list` marks the length preferences (`keyboardIncrement`, `cornerRadius`, `pasteOffset`, `gridlineEvery`,
|
||||
`typeSizeIncrement`, `baselineShiftIncrement`) with `measure: "general"|"type"`; they are kept in points and also take
|
||||
a string with a unit. Unit names: `Points`, `Picas`, `Inches`, `Millimeters`, `Centimeters`, `Pixels`, `Feet & Inches`,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue