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 text;
|
||||||
pub mod trace;
|
pub mod trace;
|
||||||
pub mod variables;
|
pub mod variables;
|
||||||
|
pub mod xml_id;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
|
||||||
|
|
@ -978,6 +978,12 @@ impl Node {
|
||||||
_ => format!("<{}>", self.kind_label()),
|
_ => 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.
|
/// Path data for path nodes.
|
||||||
pub fn path_data(&self) -> Option<&PathData> {
|
pub fn path_data(&self) -> Option<&PathData> {
|
||||||
match &self.kind {
|
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…",
|
"Layer Options…",
|
||||||
["Window", "Layers"],
|
["Window", "Layers"],
|
||||||
None,
|
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,
|
has_doc,
|
||||||
set_props
|
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`]).
|
/// 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 let Some(v) = str_param(p, "name") {
|
||||||
if v.trim().is_empty() {
|
if v.trim().is_empty() {
|
||||||
// A layer keeps a name; an object goes back to its generated `<Kind>` one.
|
// 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;
|
n.name = None;
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
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`.
|
/// 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") {
|
if let Some(v) = p.get("color") {
|
||||||
layer_color_param(v).map_err(|e| bad(cmd, e))?;
|
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"));
|
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(())
|
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.
|
/// 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> {
|
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 name = str_param(p, "name").filter(|n| !n.trim().is_empty()).map(str::to_string);
|
||||||
let id = s.edit(label, |d, _| {
|
let id = s.edit(label, |d, _| {
|
||||||
let (parent, index) = place(d)?;
|
let (parent, index) = place(d)?;
|
||||||
let mut n = make_layer(d, name.as_deref());
|
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)?)
|
Ok(d.insert(parent, index, n)?)
|
||||||
})?;
|
})?;
|
||||||
let st = s.doc_mut()?;
|
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> {
|
fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
const C: &str = "layer.setProps";
|
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)?;
|
let ids = rows_param(s, p, C)?;
|
||||||
if ids.is_empty() {
|
if ids.is_empty() {
|
||||||
return Err(bad(C, "no rows"));
|
return Err(bad(C, "no rows"));
|
||||||
}
|
}
|
||||||
s.edit("Layer Options", |d, sel| {
|
s.edit("Layer Options", |d, sel| {
|
||||||
for id in &ids {
|
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);
|
sel.deselect_uneditable(d);
|
||||||
Ok(())
|
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");
|
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]
|
#[test]
|
||||||
fn object_set_props_deselects_the_objects_it_hides_or_locks() {
|
fn object_set_props_deselects_the_objects_it_hides_or_locks() {
|
||||||
let mut s = session();
|
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
|
/// An object name from its XML id (`AI10_ArtUID`).
|
||||||
/// character; ids made unique end with `_` and 20 or more digits.
|
|
||||||
pub(super) fn xml_name(id: &str) -> String {
|
pub(super) fn xml_name(id: &str) -> String {
|
||||||
let id = strip_unique(id);
|
vectorcraft_doc::xml_id::to_object_name(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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,10 @@ fn color_field(c: LayerColor) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The fields for `n` (a layer or any other row).
|
/// 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();
|
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("visible".into(), json!(n.visible));
|
||||||
f.insert("locked".into(), json!(n.locked));
|
f.insert("locked".into(), json!(n.locked));
|
||||||
f.insert("layer".into(), json!(n.is_layer()));
|
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 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("ids".into(), json!(ids.iter().map(|i| i.0).collect::<Vec<_>>()));
|
||||||
f.insert("mode".into(), json!("edit"));
|
f.insert("mode".into(), json!("edit"));
|
||||||
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields: f });
|
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 sublayer = p.get("sublayer").and_then(Value::as_bool).unwrap_or(false);
|
||||||
let color = LayerColor::Preset((layers.len() % LAYER_COLORS.len()) as u8);
|
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!({
|
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,
|
"template": false, "locked": false, "visible": true, "printable": true, "preview": true, "dimImages": false, "dimPercent": 50,
|
||||||
});
|
});
|
||||||
app.ui.dialog = Some(Dialog::new(KIND, fields));
|
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) {
|
if view.opts.thumb(n) {
|
||||||
x = row_thumb(&ui, view, n, r, x).right() + 4.0;
|
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);
|
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>,
|
column: Option<ColumnDrag>,
|
||||||
/// The row to scroll into view.
|
/// The row to scroll into view.
|
||||||
reveal: Option<NodeId>,
|
reveal: Option<NodeId>,
|
||||||
|
/// Preferences › Units › Identify Objects By › XML ID.
|
||||||
|
identify_xml_id: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What one frame of the panel decided.
|
/// 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(),
|
h: app.ui.layers_panel.row(),
|
||||||
column: ui.data(|d| d.get_temp(key("column"))),
|
column: ui.data(|d| d.get_temp(key("column"))),
|
||||||
reveal,
|
reveal,
|
||||||
|
identify_xml_id: app.session.prefs.identify_objects_by == "xmlId",
|
||||||
};
|
};
|
||||||
let mut out = Out::default();
|
let mut out = Out::default();
|
||||||
ui.add_space(6.0);
|
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;
|
x = th.right() + 4.0;
|
||||||
th
|
th
|
||||||
});
|
});
|
||||||
// Name.
|
// Name (Preferences › Units › Identify Objects By: Object Name or XML ID).
|
||||||
let name = if doc.mask_edit.is_some_and(|m| m.layer == n.id) { "<Opacity Mask>".to_string() } else { n.display_name() };
|
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 font = egui::FontId::proportional(13.0);
|
||||||
let renaming: Option<(u64, String)> = ui.data(|d| d.get_temp(rename_id()));
|
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));
|
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);
|
let painter = ui.painter().with_clip_rect(name_rect);
|
||||||
// Generated names ("<Path>", "<Opacity Mask>") are translated where painted; the stored name stays English.
|
// 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) {
|
let shown = if doc.mask_edit.is_some_and(|m| m.layer == n.id) {
|
||||||
tl!("<Opacity Mask>").to_string()
|
tl!("<Opacity Mask>").to_string()
|
||||||
|
} else if xml_id {
|
||||||
|
name.clone()
|
||||||
} else {
|
} 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 });
|
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();
|
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_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)));
|
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 {
|
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 {
|
} else {
|
||||||
out.actions.push(("ui.layerOptions".into(), json!({"ids": [n.id.0]})));
|
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;
|
return;
|
||||||
};
|
};
|
||||||
// Owned snapshots: the rows below run commands, which borrow the app mutably.
|
// 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
|
let vars: Vec<VarRow> = st
|
||||||
.doc
|
.doc
|
||||||
.variables
|
.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 bound: Vec<u64> = st.doc.variables.objects_of(&v.name).iter().map(|i| i.0).collect();
|
||||||
let object = match bound.as_slice() {
|
let object = match bound.as_slice() {
|
||||||
[] => String::new(),
|
[] => 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(),
|
_ => tl!("Multiple objects").to_string(),
|
||||||
};
|
};
|
||||||
VarRow { name: v.name.clone(), kind: v.kind, bound, object }
|
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
|
`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
|
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.
|
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`,
|
`prefs.list` marks the length preferences (`keyboardIncrement`, `cornerRadius`, `pasteOffset`, `gridlineEvery`,
|
||||||
`typeSizeIncrement`, `baselineShiftIncrement`) with `measure: "general"|"type"`; they are kept in points and also take
|
`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`,
|
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