diff --git a/crates/engine/src/cmd/library.rs b/crates/engine/src/cmd/library.rs index a6d2cbf518..ae8a7ef6b2 100644 --- a/crates/engine/src/cmd/library.rs +++ b/crates/engine/src/cmd/library.rs @@ -346,6 +346,36 @@ impl Library { } } + /// Name item `r` `name`, in its place in its kind's list; an item keyed by its name keeps its + /// group under the new name (a graphic is keyed by its id, which stays). + fn rename_item(&mut self, r: &ItemRef, name: &str) { + fn set<'a>(mut names: impl Iterator, from: &str, to: &str) { + if let Some(n) = names.find(|n| n.as_str() == from) { + *n = to.to_string(); + } + } + let from = r.item.as_str(); + match r.kind { + ItemKind::Color => set(self.colors.iter_mut().map(|c| &mut c.name), from, name), + ItemKind::Gradient => set(self.gradients.iter_mut().map(|g| &mut g.name), from, name), + ItemKind::Pattern => set(self.patterns.iter_mut().map(|p| &mut p.name), from, name), + ItemKind::CharStyle => set(self.char_styles.iter_mut().map(|t| &mut t.name), from, name), + ItemKind::ParaStyle => set(self.para_styles.iter_mut().map(|t| &mut t.name), from, name), + ItemKind::Graphic => { + if let Some(g) = self.graphics.iter_mut().find(|g| g.id == from) { + g.name = name.to_string(); + } + return; + } + ItemKind::Unknown => return, + } + for x in self.groups.iter_mut().flat_map(|g| g.items.iter_mut()) { + if x == r { + x.item = name.to_string(); + } + } + } + /// Move item `r` next to `to`, an item of its kind: just before it, or just after it when /// `after`, in their kind's list (the order the panel shows them in). fn place_next_to(&mut self, r: &ItemRef, to: &ItemRef, after: bool) { @@ -683,7 +713,7 @@ pub fn specs() -> Vec { "Use Library Item", [], None, - "{library?, kind, item: name (graphic: id or name), center?: [x, y] (graphic; default: the first artboard's centre), to?: fill|stroke (colors, gradients, patterns; default: the kind's), recorded?: the item as it was when used (recorded in the journal so a replay matches)} graphic: place a copy centred on `center`, its resources pasted along, selected, one undo step; colors and gradients: paint the selection (and the defaults) as paint.setFill/paint.setStroke, a gradient fitting each object; a spot color links to the document's spot swatch of its ink's name, added when the document has none of that colour (a name taken by another colour gets a number), one undo step; patterns: add the pattern and its swatch to the document (a name the document gives another pattern gets a number; the same pattern is reused) and paint the selection (and the defaults) with it, one undo step; charStyle/paraStyle: add the style to the document (a name taken by different attributes gets a number) and apply it to the selected text, one undo step → {kind, ids?, style?}", + "{library?, kind, item: name (graphic: id or name), center?: [x, y] (graphic; default: the first artboard's centre), to?: fill|stroke (colors, gradients, patterns; default: the kind's), recorded?: the item as it was when used (recorded in the journal so a replay matches)} graphic: place a copy centred on `center`, its resources pasted along, selected, one undo step; colors and gradients: paint the selection (and the defaults) as paint.setFill/paint.setStroke, a gradient fitting each object; a spot color links to the document's spot swatch of its ink's name, added when the document has none of that colour (a name taken by another colour gets a number), one undo step; patterns: add the pattern and its swatch to the document (a name the document gives another pattern gets a number; the same pattern is reused) and paint the selection (and the defaults) with it, one undo step; charStyle/paraStyle: add the style to the document (a name taken by different attributes gets a number; a character style's color linked to a swatch that isn't a global or spot swatch of the document keeps the color it shows, unlinked) and apply it to the selected text, one undo step → {kind, ids?, style?}", has_doc, use_item ), @@ -696,6 +726,24 @@ pub fn specs() -> Vec { always, remove_item ), + cmd!( + "library.renameItem", + "Rename Library Item", + [], + None, + "{library?, kind, item: name (graphic: id or name), name: string (trimmed; 1 to 256 characters)} rename an item; it keeps its place and its group; another item of its kind with that name is an error, and its own name changes nothing; not undoable → {library, kind, item: its key now (graphic: its id), name}", + always, + rename_item + ), + cmd!( + "library.addToCharacterStyles", + "Add to Character Styles", + [], + None, + "{library?, item: a character style's name, recorded?: the style as it was when added (recorded in the journal so a replay matches)} add a library character style to the document's character styles without applying it (no text needs to be selected): a document style of that name with the same attributes is reused, a name taken by different attributes gets a number; a color linked to a swatch that isn't a global or spot swatch of the document keeps the color it shows, unlinked; a new style is one undo step, a reused one none → {style: its name in the document, existing: bool}", + has_doc, + add_to_char_styles + ), cmd!( "library.createGroup", "New Library Group", @@ -1231,8 +1279,13 @@ fn use_item(s: &mut Session, p: &Value) -> Result { Kind::CharStyle | Kind::ParaStyle => { let para = kind == Kind::ParaStyle; let name = data.get("name").and_then(Value::as_str).map(str::trim).filter(|n| !n.is_empty()).unwrap_or("Style").to_string(); - let attrs = data.get("attrs").and_then(Value::as_object).cloned().unwrap_or_default(); + let mut attrs = data.get("attrs").and_then(Value::as_object).cloned().unwrap_or_default(); let doc = &s.doc()?.doc; + if !para { + unlink_missing_swatches(doc, &mut attrs); + } + // Compared in the form the document stores them in. + let attrs = super::textstyles::stored_style_attrs(doc, para, &attrs).unwrap_or(attrs); // The document's style of that name when it is the same; else a new one. let same = super::textstyles::style_attrs(doc, para, &name).is_some_and(|a| a == attrs); let style = if same { name } else { unique(&name, |n| super::textstyles::style_attrs(doc, para, n).is_some()) }; @@ -1361,6 +1414,109 @@ fn remove_item(s: &mut Session, p: &Value) -> Result { ok() } +fn rename_item(s: &mut Session, p: &Value) -> Result { + const C: &str = "library.renameItem"; + let (id, kind, item) = item_param(s, p, C)?; + let name = match p.get("name") { + None | Some(Value::Null) => return Err(bad(C, "missing `name`")), + Some(Value::String(n)) => n.trim().to_string(), + Some(_) => return Err(bad(C, "`name` must be a string")), + }; + if name.is_empty() { + return Err(bad(C, "missing `name`")); + } + if name.chars().count() > MAX_NAME { + let (label, max) = ("`name`", MAX_NAME); + return Err(bad(C, format!("{label} must be at most {max} characters"))); + } + let lib = s.libraries.get(&id).ok_or_else(|| bad(C, format!("no library `{id}`")))?; + let r = lib.item_ref(kind.item_kind(), item).ok_or_else(|| bad(C, format!("the library has no item `{item}`")))?; + // The item's name now, and whether another item of its kind has the new one. + let mut same = false; + let mut taken = false; + for (_, key, shown) in lib.items().filter(|(k, ..)| *k == r.kind) { + if key == r.item { + same = shown == name; + } else if shown == name { + taken = true; + } + } + let key = if r.kind == ItemKind::Graphic { r.item.clone() } else { name.clone() }; + let out = json!({ "library": id, "kind": str_param(p, "kind"), "item": key, "name": name }); + if same { + return Ok(out); + } + if taken { + return Err(bad(C, format!("the library has another item of that kind named `{name}`"))); + } + s.libraries.change(&id, |lib| { + lib.rename_item(&r, &name); + Ok(()) + })?; + Ok(out) +} + +fn add_to_char_styles(s: &mut Session, p: &Value) -> Result { + const C: &str = "library.addToCharacterStyles"; + // A replay (the journal) carries what the style was, so it doesn't depend on the library. + let data = match p.get("recorded").cloned() { + Some(r) => r, + None => { + let id = library_id(s, p, C)?; + let item = str_param(p, "item").ok_or_else(|| bad(C, "missing `item`"))?; + let lib = s.libraries.get(&id).ok_or_else(|| bad(C, format!("no library `{id}`")))?; + let t = lib.char_styles.iter().find(|t| t.name == item).ok_or_else(|| bad(C, format!("the library has no item `{item}`")))?; + let data = json!({ "name": t.name, "attrs": t.attrs }); + s.note_journal("recorded", data.clone()); + data + } + }; + let name = data.get("name").and_then(Value::as_str).map(str::trim).filter(|n| !n.is_empty()).unwrap_or("Style").to_string(); + let mut attrs = data.get("attrs").and_then(Value::as_object).cloned().ok_or_else(|| bad(C, "the library item is damaged"))?; + let doc = &s.doc()?.doc; + unlink_missing_swatches(doc, &mut attrs); + // Compared in the form the document stores them in. + let attrs = super::textstyles::stored_style_attrs(doc, false, &attrs).unwrap_or(attrs); + // The document's style of that name when it is the same; else a new one, which isn't applied. + if super::textstyles::style_attrs(doc, false, &name).is_some_and(|a| a == attrs) { + return Ok(json!({ "style": name, "existing": true })); + } + let style = unique(&name, |n| super::textstyles::style_attrs(doc, false, n).is_some()); + s.execute("charStyle.new", &json!({ "name": style, "attrs": attrs }))?; + Ok(json!({ "style": style, "existing": false })) +} + +/// Unlink a library character style's `fill` and `stroke` colors (solid, and each gradient stop) +/// from swatches that are not global or spot swatches of `doc`: an unlinked color keeps the color +/// it shows at full tint, as [`clean_gradient`] keeps a library gradient's stops. A link to a +/// global or spot swatch `doc` has stays, and takes that swatch's color. +fn unlink_missing_swatches(doc: &vectorcraft_doc::Document, attrs: &mut Map) { + let keep = |name: &str| doc.global_color(name).is_some(); + for key in ["fill", "stroke"] { + let Some(mut paint) = attrs.get(key).and_then(|v| serde_json::from_value::(v.clone()).ok()) else { continue }; + let mut changed = false; + let mut unlink = |swatch: &mut Option, tint: &mut f32| { + if swatch.as_deref().is_some_and(|n| !keep(n)) { + *swatch = None; + *tint = 1.0; + changed = true; + } + }; + match &mut paint { + Paint::Solid { swatch, tint, .. } => unlink(swatch, tint), + Paint::Gradient(g) => { + for st in &mut g.gradient.stops { + unlink(&mut st.swatch, &mut st.tint); + } + } + _ => {} + } + if changed && let Ok(v) = serde_json::to_value(&paint) { + attrs.insert(key.to_string(), v); + } + } +} + /// `p`'s `name`, trimmed and clipped, else `default`. fn name_param(p: &Value, default: &str) -> String { let mut name = str_param(p, "name").map(str::trim).filter(|n| !n.is_empty()).unwrap_or(default).to_string(); diff --git a/crates/engine/src/cmd/textstyles.rs b/crates/engine/src/cmd/textstyles.rs index 99cfc0a3fc..8134976144 100644 --- a/crates/engine/src/cmd/textstyles.rs +++ b/crates/engine/src/cmd/textstyles.rs @@ -506,6 +506,13 @@ pub(crate) fn style_attrs(d: &Document, para: bool, name: &str) -> Option) -> Option> { + let kind = if para { Kind::Para } else { Kind::Char }; + attrs_param(d, &json!({ "attrs": attrs }), kind, "new", None).ok().flatten() +} + /// The name `charStyle.new` (`para`: `paraStyle.new`) gives a style when it is given none: the /// first "Character Style N" ("Paragraph Style N") that no character style (paragraph style) of the /// document has. diff --git a/crates/engine/src/tests/library.rs b/crates/engine/src/tests/library.rs index 15a8da8104..d7927ec6bf 100644 --- a/crates/engine/src/tests/library.rs +++ b/crates/engine/src/tests/library.rs @@ -635,6 +635,210 @@ fn a_library_style_named_with_spaces_comes_in_trimmed() { assert_eq!((text.runs[0].style.size, text.runs[0].style.style_name.as_deref()), (31.0, Some("Body"))); } +/// The size of the first run of text object `id`. +fn run_size(s: &Session, id: &Value) -> f64 { + match &s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind { + vectorcraft_doc::NodeKind::Text(t) => t.runs[0].style.size, + _ => 0.0, + } +} + +/// Add to Character Styles puts a library character style into the document's character styles +/// without applying it, with or without type selected, in one undo step; the same style there is +/// reused without a step, and a name taken by other attributes gets a number. A bad call changes +/// nothing. +#[test] +fn a_library_character_style_is_added_to_the_document_without_applying_it() { + let mut s = session(); + let data = json!({"name": "Kept", "charStyles": [{"name": "Headline", "attrs": {"size": 31.0}}, {"name": "Broken", "attrs": {"size": "big"}}]}); + let lib = run(&mut s, "library.import", json!({"data": data.to_string()}))["id"].as_str().unwrap().to_string(); + let styles = |s: &Session| s.doc().unwrap().doc.char_styles.iter().map(|c| c.name.clone()).collect::>(); + let undo = |s: &Session| s.doc().unwrap().history.undo.len(); + // Nothing selected. + let before = undo(&s); + let r = run(&mut s, "library.addToCharacterStyles", json!({"library": lib, "item": "Headline"})); + assert_eq!((r["style"].as_str(), r["existing"].as_bool()), (Some("Headline"), Some(false))); + assert_eq!(styles(&s), ["Headline"]); + assert_eq!(undo(&s), before + 1, "one undo step"); + let attrs = &s.doc().unwrap().doc.char_styles[0].attrs; + assert_eq!(attrs.get("size"), Some(&json!(31.0))); + // Selected type keeps its look. + let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Body", "size": 9}))["id"].clone(); + run(&mut s, "select.set", json!({"ids": [t]})); + let before = undo(&s); + let r = run(&mut s, "library.addToCharacterStyles", json!({"library": lib, "item": "Headline"})); + assert_eq!((r["style"].as_str(), r["existing"].as_bool()), (Some("Headline"), Some(true))); + assert_eq!((undo(&s), run_size(&s, &t), styles(&s).len()), (before, 9.0, 1), "reused: no step, the type unchanged"); + // Another style of that name: numbered, still not applied. + run(&mut s, "charStyle.setAttrs", json!({"name": "Headline", "attrs": {"size": 5}})); + let r = run(&mut s, "library.addToCharacterStyles", json!({"library": lib, "item": "Headline"})); + assert_eq!((r["style"].as_str(), r["existing"].as_bool()), (Some("Headline 2"), Some(false))); + assert_eq!(run_size(&s, &t), 9.0); + let vectorcraft_doc::NodeKind::Text(text) = &s.doc().unwrap().doc.node(NodeId(t.as_u64().unwrap())).unwrap().kind else { panic!("not text") }; + assert_eq!(text.runs[0].style.style_name, None, "no style applied"); + // Undo takes the added style back alone. + run(&mut s, "edit.undo", json!({})); + assert_eq!(styles(&s), ["Headline"]); + // Bad calls: nothing changes. + let before = (undo(&s), styles(&s)); + for bad in [ + json!({"library": lib}), + json!({"library": lib, "item": 3}), + json!({"library": lib, "item": "Nope"}), + json!({"library": "Nope", "item": "Headline"}), + json!({"library": lib, "item": "Broken"}), + json!({"recorded": {"name": "X", "attrs": "damaged"}}), + ] { + assert!(s.execute("library.addToCharacterStyles", &bad).is_err(), "{bad}"); + assert_eq!((undo(&s), styles(&s)), before, "{bad}"); + } + // Without a document it isn't available. + run(&mut s, "file.close", json!({})); + assert!(s.active().is_none()); + assert!(s.execute("library.addToCharacterStyles", &json!({"library": lib, "item": "Headline"})).is_err()); +} + +/// A library character style whose fill links to a swatch the document lacks is added and applied +/// with the color it shows, unlinked at full tint; a global swatch of that name in the document +/// keeps the link and gives its color. +#[test] +fn a_library_character_style_linked_to_a_missing_swatch_is_added_unlinked() { + let base = vectorcraft_color::Color::rgb(0.2, 0.4, 0.6); + let shown = base.tinted(0.5); + let linked = vectorcraft_color::Paint::Solid { color: shown, swatch: Some("Brand".into()), tint: 0.5 }; + let data = json!({"name": "Kept", "charStyles": [{"name": "Accent", "attrs": {"size": 20.0, "fill": linked}}]}); + let mut s = session(); + let lib = run(&mut s, "library.import", json!({"data": data.to_string()}))["id"].as_str().unwrap().to_string(); + let fill = |s: &Session, name: &str| { + let st = s.doc().unwrap().doc.char_styles.iter().find(|c| c.name == name).map(|c| c.attrs.clone()).unwrap(); + serde_json::from_value::(st["fill"].clone()).unwrap() + }; + run(&mut s, "library.addToCharacterStyles", json!({"library": lib, "item": "Accent"})); + assert_eq!(fill(&s, "Accent"), vectorcraft_color::Paint::Solid { color: shown, swatch: None, tint: 1.0 }); + // Apply as well, on selected type. + let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Body", "size": 9}))["id"].clone(); + run(&mut s, "select.set", json!({"ids": [t]})); + let r = run(&mut s, "library.use", json!({"library": lib, "kind": "charStyle", "item": "Accent"})); + assert_eq!(r["style"], "Accent", "the added style is reused"); + assert_eq!(run_size(&s, &t), 20.0); + // A document with a global swatch of that name keeps the link. + run(&mut s, "file.new", json!({"width": 200, "height": 200})); + let other = vectorcraft_color::Color::rgb(0.8, 0.1, 0.1); + run(&mut s, "swatch.new", json!({"name": "Brand", "color": other, "global": true})); + run(&mut s, "library.addToCharacterStyles", json!({"library": lib, "item": "Accent"})); + let vectorcraft_color::Paint::Solid { swatch, tint, .. } = fill(&s, "Accent") else { panic!("not a solid fill") }; + assert_eq!((swatch.as_deref(), tint), (Some("Brand"), 0.5)); + // The style with the document's swatch color is the same style: added again, it is reused. + let r = run(&mut s, "library.addToCharacterStyles", json!({"library": lib, "item": "Accent"})); + assert_eq!((r["style"].as_str(), r["existing"].as_bool()), (Some("Accent"), Some(true))); +} + +/// A library style is compared with the document's in the form the document stores it in, so a +/// style whose file writes a size as a whole number is reused, by Add to Character Styles and by a +/// click alike. +#[test] +fn a_library_style_is_reused_whatever_form_its_file_writes_it_in() { + let data = json!({"name": "Kept", "charStyles": [{"name": "Whole", "attrs": {"size": 31}}], "paraStyles": [{"name": "Para", "attrs": {"left_indent": 4}}]}); + let mut s = session(); + let lib = run(&mut s, "library.import", json!({"data": data.to_string()}))["id"].as_str().unwrap().to_string(); + run(&mut s, "library.addToCharacterStyles", json!({"library": lib, "item": "Whole"})); + let r = run(&mut s, "library.addToCharacterStyles", json!({"library": lib, "item": "Whole"})); + assert_eq!((r["style"].as_str(), r["existing"].as_bool()), (Some("Whole"), Some(true))); + let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Body", "size": 9}))["id"].clone(); + run(&mut s, "select.set", json!({"ids": [t]})); + for (kind, item) in [("charStyle", "Whole"), ("paraStyle", "Para"), ("paraStyle", "Para")] { + assert_eq!(run(&mut s, "library.use", json!({"library": lib, "kind": kind, "item": item}))["style"], item); + } + let d = &s.doc().unwrap().doc; + assert_eq!((d.char_styles.len(), d.para_styles.iter().filter(|p| p.name.starts_with("Para")).count()), (1, 1)); +} + +/// Rename gives a library item a new name, trimmed, in its place and its group; a character style +/// keeps its attributes, a graphic its id. Another item of its kind with that name, an empty or +/// too long name, a name that isn't text and an unknown item or library are errors that change +/// nothing; a name with path characters is a name like any other; the item's own name writes +/// nothing; and the document's history is not touched. +#[test] +fn library_items_are_renamed_in_place() { + let (mut s, g, c, st) = brand(); + let t = run(&mut s, "text.create", json!({"x": 10, "y": 120, "text": "Body", "size": 9}))["id"].clone(); + run(&mut s, "select.set", json!({"ids": [t]})); + let other = run(&mut s, "library.add", json!({"kind": "charStyle", "name": "Body"}))["name"].as_str().unwrap().to_string(); + run(&mut s, "library.createGroup", json!({"name": "Brand", "items": [{"kind": "charStyle", "item": st}, {"kind": "fillColor", "item": c}]})); + let undo = s.doc().unwrap().history.undo.len(); + let attrs = run(&mut s, "library.get", json!({}))["charStyles"][0]["attrs"].clone(); + let r = run(&mut s, "library.renameItem", json!({"kind": "charStyle", "item": st, "name": " Title "})); + assert_eq!((r["item"].as_str(), r["name"].as_str()), (Some("Title"), Some("Title"))); + let got = run(&mut s, "library.get", json!({})); + assert_eq!(got["charStyles"][0], json!({"name": "Title", "attrs": attrs}), "first still, the attributes kept"); + assert_eq!(got["charStyles"][1]["name"], other.as_str()); + assert_eq!(groups(&mut s), [("Brand".into(), refs(&[("charStyle", "Title"), ("color", &c)]))]); + // A colour keeps its group under its new name; a graphic keeps its id. + run(&mut s, "library.renameItem", json!({"kind": "strokeColor", "item": c, "name": "Navy"})); + assert_eq!(groups(&mut s), [("Brand".into(), refs(&[("charStyle", "Title"), ("color", "Navy")]))]); + let r = run(&mut s, "library.renameItem", json!({"kind": "graphic", "item": "Logo", "name": "Mark"})); + assert_eq!(r["item"], g.as_str()); + let graphic = run(&mut s, "library.get", json!({}))["graphics"][0].clone(); + assert_eq!((graphic["id"].as_str(), graphic["name"].as_str()), (Some(g.as_str()), Some("Mark"))); + // Path characters are kept. + run(&mut s, "library.renameItem", json!({"kind": "charStyle", "item": "Title", "name": "a/b\\c:d"})); + assert_eq!(style_order_of(&mut s)[0], "a/b\\c:d"); + // Errors change nothing. + let before = run(&mut s, "library.get", json!({})); + let long = "x".repeat(257); + for bad in [ + json!({"kind": "charStyle", "item": "a/b\\c:d", "name": other}), + json!({"kind": "charStyle", "item": "a/b\\c:d", "name": " "}), + json!({"kind": "charStyle", "item": "a/b\\c:d", "name": long}), + json!({"kind": "charStyle", "item": "a/b\\c:d", "name": 5}), + json!({"kind": "charStyle", "item": "a/b\\c:d"}), + json!({"kind": "charStyle", "item": "Nope", "name": "X"}), + json!({"kind": "paraStyle", "item": "a/b\\c:d", "name": "X"}), + json!({"kind": "brush", "item": "a/b\\c:d", "name": "X"}), + json!({"library": "Nope", "kind": "charStyle", "item": "a/b\\c:d", "name": "X"}), + ] { + assert!(s.execute("library.renameItem", &bad).is_err(), "{bad}"); + assert_eq!(run(&mut s, "library.get", json!({})), before, "{bad}"); + } + // A style of another kind may have the name, and 256 characters are allowed. + run(&mut s, "library.renameItem", json!({"kind": "charStyle", "item": "a/b\\c:d", "name": "Navy"})); + run(&mut s, "library.renameItem", json!({"kind": "charStyle", "item": "Navy", "name": "y".repeat(256)})); + assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no document undo step"); +} + +/// The current library's character style names, in order. +fn style_order_of(s: &mut Session) -> Vec { + order(s, "charStyles", "name") +} + +/// A rename is kept in the library's file; renaming an item to its own name writes nothing; and a +/// rename whose file can't be written is taken back. +#[cfg(not(target_arch = "wasm32"))] +#[test] +fn a_library_item_rename_is_saved_or_taken_back() { + let dir = vectorcraft_testkit::temp_dir("library-rename-item"); + let folder = dir.to_string_lossy().to_string(); + let mut s = session(); + s.libraries.set_dir(Some(folder.clone())); + let lib = run(&mut s, "library.create", json!({"name": "Kept"}))["id"].as_str().unwrap().to_string(); + let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Body", "size": 12}))["id"].clone(); + run(&mut s, "select.set", json!({"ids": [t]})); + run(&mut s, "library.add", json!({"kind": "charStyle", "name": "Body"})); + run(&mut s, "library.renameItem", json!({"kind": "charStyle", "item": "Body", "name": "Text"})); + let mut t2 = session(); + t2.libraries.set_dir(Some(folder.clone())); + assert_eq!(style_order_of(&mut t2), ["Text"], "kept in the file"); + // The file can't be written (a folder stands where it goes): its own name still succeeds, as + // it writes nothing; a new name fails and is taken back. + let file = dir.join(format!("{lib}.vclibrary")); + std::fs::remove_file(&file).unwrap(); + std::fs::create_dir(&file).unwrap(); + run(&mut s, "library.renameItem", json!({"kind": "charStyle", "item": "Text", "name": " Text "})); + assert!(s.execute("library.renameItem", &json!({"kind": "charStyle", "item": "Text", "name": "Copy"})).is_err()); + assert_eq!(style_order_of(&mut s), ["Text"], "taken back"); + let _ = std::fs::remove_dir_all(&dir); +} + #[cfg(not(target_arch = "wasm32"))] #[test] fn libraries_are_kept_in_files_across_sessions() { diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 3828d9ba2d..1fb96d9c7d 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -3491,6 +3491,8 @@ digits Malayalam Малаялам Rename Library Group Преименуване на библиотечна група Delete Library Group Изтриване на библиотечна група Move Library Item to Group Преместване на библиотечен елемент в група + Rename Library Item Преименуване на елемент от библиотека + Add to Character Styles Добавяне към стилове на знаци Library Библиотека Rename Library Преименуване на библиотека Show Library Показване на библиотека @@ -4064,6 +4066,7 @@ axis Both И двете Drag to the window's left or right edge to dock Плъзнете до левия или десния ръб на прозореца за закотвяне Float Tool Group Плаваща група инструменти @msg no library `{id}` няма библиотека `{id}` +@msg the library has another item of that kind named `{name}` библиотеката вече има друг елемент от този вид с име `{name}` @msg select some type first първо изберете текст @msg kerning must be a number, "auto", or "optical" kerning трябва да е число, „auto“ или „optical“ @msg {_1} has no {_2} style {_1} няма стил {_2} diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 81fee3a079..f04e9535e5 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -3550,6 +3550,8 @@ theme Appearance Izgled Rename Library Group Preimenuj grupu biblioteke Delete Library Group Izbriši grupu biblioteke Move Library Item to Group Premjesti stavku biblioteke u grupu + Rename Library Item Preimenuj stavku biblioteke + Add to Character Styles Dodaj u stilove znakova Rename Library Preimenuj biblioteku Show Library Prikaži biblioteku Use Library Item Koristi stavku biblioteke @@ -4416,6 +4418,7 @@ imageTrace Outline Obris @plural {n} step|{n} steps {n} korak|{n} koraka|{n} koraka @plural {n} stroke|{n} strokes {n} potez|{n} poteza|{n} poteza @msg no library `{id}` nema biblioteke `{id}` +@msg the library has another item of that kind named `{name}` biblioteka već ima drugu stavku te vrste s imenom `{name}` @msg select some type first prvo odaberite tekst @msg kerning must be a number, "auto", or "optical" kerning mora biti broj, „auto” ili „optical” @msg the selection has no gradient fill odabir nema ispunu gradijentom diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 02ae0f4a09..e6b6f398f2 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2634,6 +2634,8 @@ digits Malayalam Malaiàlam Move in Perspective Mou en perspectiva Move Library Group Mou el grup de la biblioteca Move Library Item to Group Mou l’element de la biblioteca al grup + Rename Library Item Canvia el nom de l’element de la biblioteca + Add to Character Styles Afegeix als estils de caràcter Move Locked and Hidden Artwork with Artboard Mou la il·lustració bloquejada i oculta amb el tauler Move Mesh Point Mou el punt de malla Move Mesh Points Mou els punts de malla @@ -5730,6 +5732,7 @@ digits Malayalam Malaiàlam @msg no interaction in progress no hi ha cap interacció en curs @msg no key object no hi ha objecte clau @msg no library `{id}` no hi ha la biblioteca `{id}` +@msg the library has another item of that kind named `{name}` la biblioteca ja té un altre element d’aquest tipus anomenat `{name}` @msg no library `{key}` (see {list}) no hi ha la biblioteca `{key}` (vegeu {list}) @msg No linked files to look for No hi ha fitxers enllaçats per buscar @msg no Live Paint edge near that point no hi ha cap aresta de pintura interactiva a prop d’aquell punt diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 7e3794e8a1..a44e3b9979 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3604,6 +3604,8 @@ theme Appearance Motiv Rename Library Group Přejmenovat skupinu knihovny Delete Library Group Odstranit skupinu knihovny Move Library Item to Group Přesunout položku knihovny do skupiny + Rename Library Item Přejmenovat položku knihovny + Add to Character Styles Přidat do stylů znaků Rename Library Přejmenovat knihovnu Show Library Zobrazit knihovnu Use Library Item Použít položku knihovny @@ -4477,6 +4479,7 @@ imageTrace Outline Obrys # Status and error messages @msg no library `{id}` knihovna `{id}` neexistuje +@msg the library has another item of that kind named `{name}` knihovna už má jinou položku tohoto druhu s názvem `{name}` @msg select some type first nejprve vyberte text @msg kerning must be a number, "auto", or "optical" kerning musí být číslo, „auto“ nebo „optical“ @msg the selection has no gradient fill výběr nemá přechodovou výplň diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index f7011e23bf..bce3bd12da 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -3337,6 +3337,8 @@ theme Appearance Udseende Rename Library Group Omdøb biblioteksgruppe Delete Library Group Slet biblioteksgruppe Move Library Item to Group Flyt bibliotekselement til gruppe + Rename Library Item Omdøb bibliotekselement + Add to Character Styles Føj til tegnstile Rename Library Omdøb bibliotek Show Library Vis bibliotek Use Library Item Brug bibliotekselement @@ -4183,6 +4185,7 @@ imageTrace Outline Kontur @plural {n} step|{n} steps {n} trin|{n} trin @plural {n} stroke|{n} strokes {n} kontur|{n} konturer @msg no library `{id}` intet bibliotek `{id}` +@msg the library has another item of that kind named `{name}` biblioteket har allerede et andet element af den type med navnet `{name}` @msg select some type first markér først noget tekst @msg kerning must be a number, "auto", or "optical" kerning skal være et tal, „auto“ eller „optical“ @msg {_1} has no {_2} style {_1} har ingen {_2}-stil diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 4174537f87..0d995ddc2e 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4091,6 +4091,7 @@ theme Appearance Erscheinungsbild # (`untranslated_report` lists any that are missing). Command ids, parameter # names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are. @msg no library `{id}` keine Bibliothek `{id}` +@msg the library has another item of that kind named `{name}` die Bibliothek hat bereits ein anderes Element dieser Art namens `{name}` @msg select some type first wählen Sie zuerst Text aus @msg kerning must be a number, "auto", or "optical" kerning muss eine Zahl, „auto“ oder „optical“ sein @msg the selection has no gradient fill die Auswahl hat keinen Flächenverlauf @@ -5124,6 +5125,8 @@ theme Appearance Erscheinungsbild Rename Library Group Bibliotheksgruppe umbenennen Delete Library Group Bibliotheksgruppe löschen Move Library Item to Group Bibliothekselement in Gruppe verschieben + Rename Library Item Bibliothekselement umbenennen + Add to Character Styles Zu Zeichenformaten hinzufügen Import Library Bibliothek importieren Export Library Bibliothek exportieren New Group Neue Gruppe diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index bde2fbee78..cb409e9274 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -2266,6 +2266,7 @@ axis Both Και τα δύο Document Options Επιλογές εγγράφου Dock Control Bar Αγκύρωση γραμμής ελέγχου @msg no library `{id}` Δεν υπάρχει βιβλιοθήκη με αναγνωριστικό `{id}` +@msg the library has another item of that kind named `{name}` η βιβλιοθήκη έχει ήδη άλλο στοιχείο αυτού του είδους με όνομα `{name}` @msg select some type first Επιλέξτε πρώτα κάποιο κείμενο @msg kerning must be a number, "auto", or "optical" το kerning πρέπει να είναι αριθμός, «auto» ή «optical» @msg the selection has no gradient fill η επιλογή δεν έχει ντεγκραντέ γεμίσματος @@ -4051,6 +4052,8 @@ digits Malayalam Μαλαγιάλαμ Rename Library Group Μετονομασία ομάδας βιβλιοθήκης Delete Library Group Διαγραφή ομάδας βιβλιοθήκης Move Library Item to Group Μετακίνηση στοιχείου βιβλιοθήκης σε ομάδα + Rename Library Item Μετονομασία στοιχείου βιβλιοθήκης + Add to Character Styles Προσθήκη στα στυλ χαρακτήρων Rename Library Μετονομασία βιβλιοθήκης Show Library Εμφάνιση βιβλιοθήκης Use Library Item Χρήση στοιχείου βιβλιοθήκης diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ff28d9dfc5..de9512cddb 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -3583,6 +3583,8 @@ digits Malayalam Malayálam Rename Library Group Cambiar nombre del grupo de biblioteca Delete Library Group Eliminar grupo de biblioteca Move Library Item to Group Mover elemento de la biblioteca al grupo + Rename Library Item Cambiar nombre del elemento de la biblioteca + Add to Character Styles Agregar a estilos de carácter Library Biblioteca Rename Library Cambiar nombre de la biblioteca Show Library Mostrar biblioteca @@ -4167,6 +4169,7 @@ axis Both Ambos # (`untranslated_report` lists any that are missing). Command ids, parameter # names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are. @msg no library `{id}` no hay ninguna biblioteca `{id}` +@msg the library has another item of that kind named `{name}` la biblioteca ya tiene otro elemento de ese tipo llamado `{name}` @msg select some type first selecciona primero algún texto @msg kerning must be a number, "auto", or "optical" kerning debe ser un número, «auto» u «optical» @msg the selection has no gradient fill la selección no tiene un degradado de relleno diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index bb74188ed8..1c3327fb1e 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -3542,6 +3542,8 @@ theme Appearance Ulkoasu Rename Library Group Nimeä kirjastoryhmä uudelleen Delete Library Group Poista kirjastoryhmä Move Library Item to Group Siirrä kirjaston kohde ryhmään + Rename Library Item Nimeä kirjaston kohde uudelleen + Add to Character Styles Lisää merkkityyleihin Rename Library Nimeä kirjasto uudelleen Show Library Näytä kirjasto Use Library Item Käytä kirjaston kohdetta @@ -4392,6 +4394,7 @@ imageTrace Outline Ääriviiva @plural {n} step|{n} steps {n} vaihe|{n} vaihetta @plural {n} stroke|{n} strokes {n} viiva|{n} viivaa @msg no library `{id}` ei kirjastoa `{id}` +@msg the library has another item of that kind named `{name}` kirjastossa on jo toinen samanlainen kohde nimeltä `{name}` @msg select some type first valitse ensin tekstiä @msg kerning must be a number, "auto", or "optical" kerning-arvon on oltava luku, ”auto” tai ”optical” @msg the selection has no gradient fill valinnalla ei ole liukuväritäyttöä diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 09c9c35b0d..3b45c4d386 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3575,6 +3575,8 @@ digits Malayalam Malayalam Rename Library Group Renommer le groupe de bibliothèque Delete Library Group Supprimer le groupe de bibliothèque Move Library Item to Group Déplacer l’élément de la bibliothèque vers un groupe + Rename Library Item Renommer l’élément de la bibliothèque + Add to Character Styles Ajouter aux styles de caractère Library Bibliothèque Rename Library Renommer la bibliothèque Show Library Afficher la bibliothèque @@ -4156,6 +4158,7 @@ axis Both Les deux # (`untranslated_report` lists any that are missing). Command ids, parameter # names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are. @msg no library `{id}` aucune bibliothèque `{id}` +@msg the library has another item of that kind named `{name}` la bibliothèque a déjà un autre élément de ce type nommé `{name}` @msg select some type first sélectionnez d’abord du texte @msg kerning must be a number, "auto", or "optical" kerning doit être un nombre, « auto » ou « optical » @msg the selection has no gradient fill la sélection n’a pas de dégradé de fond diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 9aa4bd7b99..0c6089e40a 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -2873,6 +2873,8 @@ antiAlias Sharp शार्प Move in Perspective पर्सपेक्टिव में मूव करें Move Library Group लाइब्रेरी ग्रुप मूव करें Move Library Item to Group लाइब्रेरी आइटम को ग्रुप में मूव करें + Rename Library Item लाइब्रेरी आइटम का नाम बदलें + Add to Character Styles कैरेक्टर स्टाइल्स में जोड़ें Move Locked and Hidden Artwork with Artboard आर्टबोर्ड के साथ लॉक्ड और छिपा हुआ आर्टवर्क मूव करें Move Mesh Point मेश पॉइंट मूव करें Move Mesh Points मेश पॉइंट मूव करें @@ -7234,6 +7236,7 @@ antiAlias Sharp शार्प @msg no layer {id} (see layer.list) कोई लेयर {id} (layer.list देखें) @msg no layers to export निर्यात करने के लिए कोई लेयरें नहीं @msg no library `{id}` नहीं लाइब्रेरी `{id}` +@msg the library has another item of that kind named `{name}` लाइब्रेरी में इस प्रकार का एक और आइटम `{name}` नाम से पहले से है @msg no library `{key}` नहीं लाइब्रेरी `{key}` @msg no library `{key}` (see {list}) कोई लाइब्रेरी `{key}` ({list} देखें) @msg No line breaks to remove हटाने के लिए कोई लाइन ब्रेक नहीं diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index af6f586880..06122f94ee 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2605,6 +2605,8 @@ file-filter Templates Sablonok Move Guide to Layer Segédvonal áthelyezése rétegre Move in Perspective Áthelyezés perspektívában Move Library Item to Group Könyvtárelem áthelyezése csoportba + Rename Library Item Könyvtárelem átnevezése + Add to Character Styles Hozzáadás a karakterstílusokhoz Move Locked and Hidden Artwork with Artboard Zárolt és rejtett grafika mozgatása a rajztáblával Move Mesh Point Hálópont áthelyezése Move Mesh Points Hálópontok áthelyezése @@ -5314,6 +5316,7 @@ imageTrace Outline Körvonal @msg no interaction in progress nincs folyamatban lévő művelet @msg no key object nincs kulcsobjektum @msg no library `{id}` nincs ilyen könyvtár: `{id}` +@msg the library has another item of that kind named `{name}` a könyvtárban már van egy másik ilyen fajta elem `{name}` néven @msg no library `{key}` (see {list}) nincs ilyen könyvtár: `{key}` (lásd: {list}) @msg no Live Paint edge near that point annál a pontnál nincs élőfestés-él @msg no Live Paint face at that point annál a pontnál nincs élőfestés-felület diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index e0ba31ecd5..00d2b09600 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -3337,6 +3337,8 @@ theme Appearance Tampilan Rename Library Group Ganti nama grup pustaka Delete Library Group Hapus grup pustaka Move Library Item to Group Pindahkan item pustaka ke grup + Rename Library Item Ganti nama item pustaka + Add to Character Styles Tambahkan ke gaya karakter Rename Library Ganti nama pustaka Show Library Tampilkan pustaka Use Library Item Gunakan item pustaka @@ -4183,6 +4185,7 @@ imageTrace Outline Kontur @plural {n} step|{n} steps {n} langkah @plural {n} stroke|{n} strokes {n} goresan @msg no library `{id}` tidak ada pustaka `{id}` +@msg the library has another item of that kind named `{name}` pustaka sudah memiliki item lain dari jenis itu bernama `{name}` @msg select some type first pilih beberapa teks terlebih dahulu @msg kerning must be a number, "auto", or "optical" kerning harus berupa angka, “auto”, atau “optical” @msg {_1} has no {_2} style {_1} tidak memiliki gaya {_2} diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index ce0bc58ce5..869fe2e4a0 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -3628,6 +3628,8 @@ digits Malayalam Malayalam Rename Library Group Rinomina gruppo di libreria Delete Library Group Elimina gruppo di libreria Move Library Item to Group Sposta elemento della libreria nel gruppo + Rename Library Item Rinomina elemento della libreria + Add to Character Styles Aggiungi agli stili carattere Library Libreria Rename Library Rinomina libreria Show Library Mostra libreria @@ -4218,6 +4220,7 @@ axis Both Entrambi # (`untranslated_report` lists any that are missing). Command ids, parameter # names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are. @msg no library `{id}` nessuna libreria `{id}` +@msg the library has another item of that kind named `{name}` la libreria ha già un altro elemento di questo tipo chiamato `{name}` @msg select some type first seleziona prima del testo @msg kerning must be a number, "auto", or "optical" kerning deve essere un numero, «auto» o «optical» @msg the selection has no gradient fill la selezione non ha una sfumatura di riempimento diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 1ecce4618a..a0ce9c93b9 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3364,6 +3364,8 @@ mark-style Japanese 日本式 Rename Library Group ライブラリグループ名を変更 Delete Library Group ライブラリグループを削除 Move Library Item to Group ライブラリ項目をグループに移動 + Rename Library Item ライブラリ項目名を変更 + Add to Character Styles 文字スタイルに追加 Library ライブラリ Rename Library ライブラリ名を変更 Show Library ライブラリを表示 @@ -4491,6 +4493,7 @@ swatch Bright Violet ビビッドバイオレット @msg the layer to flatten into is hidden 統合先のレイヤーが非表示になっています @msg there are no objects to release レイヤーに分配するオブジェクトがありません @msg no library `{id}` ライブラリ `{id}` が見つかりません +@msg the library has another item of that kind named `{name}` ライブラリには同じ種類の項目 `{name}` がすでにあります @msg {path}: not a file {path}: ファイルではありません @msg {path}: {e} {path}: {e} @msg can't write .{ext} images .{ext} 画像は書き出せません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 3d6425c373..b19ed4009a 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -3183,6 +3183,8 @@ theme Appearance იერსახე Rename Library Group ბიბლიოთეკის ჯგუფის სახელის შეცვლა Delete Library Group ბიბლიოთეკის ჯგუფის წაშლა Move Library Item to Group ბიბლიოთეკის ელემენტის ჯგუფში გადატანა + Rename Library Item ბიბლიოთეკის ელემენტის სახელის შეცვლა + Add to Character Styles სიმბოლოს სტილებში დამატება Rename Library ბიბლიოთეკის სახელის შეცვლა Show Library ბიბლიოთეკის ჩვენება Use Library Item ბიბლიოთეკის ელემენტის გამოყენება @@ -3986,6 +3988,7 @@ font-technology Variable ცვლადი @plural {n} step|{n} steps ნაბიჯი: {n} @plural {n} stroke|{n} strokes კონტური: {n} @msg no library `{id}` ბიბლიოთეკა `{id}` არ არსებობს +@msg the library has another item of that kind named `{name}` ბიბლიოთეკას უკვე აქვს ამ ტიპის სხვა ელემენტი სახელით `{name}` @msg select some type first ჯერ მონიშნეთ ტექსტი @msg kerning must be a number, "auto", or "optical" kerning უნდა იყოს რიცხვი, „auto“ ან „optical“ @msg the selection has no gradient fill მონიშნულს შევსების გრადიენტი არ აქვს diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index fb87488d64..3f518f00c4 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -3353,6 +3353,8 @@ mark-style Japanese 일본식 Rename Library Group 라이브러리 그룹 이름 바꾸기 Delete Library Group 라이브러리 그룹 삭제 Move Library Item to Group 라이브러리 항목을 그룹으로 이동 + Rename Library Item 라이브러리 항목 이름 바꾸기 + Add to Character Styles 문자 스타일에 추가 Library 라이브러리 Rename Library 라이브러리 이름 바꾸기 Show Library 라이브러리 표시 @@ -4418,6 +4420,7 @@ theme Appearance 외관 @msg the layer to flatten into is hidden 병합 대상 레이어가 숨겨져 있습니다 @msg there are no objects to release 해제할 개체가 없습니다 @msg no library `{id}` `{id}` 라이브러리가 없습니다 +@msg the library has another item of that kind named `{name}` 라이브러리에 같은 종류의 다른 항목 `{name}`이(가) 이미 있습니다 @msg {path}: not a file {path}: 파일이 아닙니다 @msg {path}: {e} {path}: {e} @msg can't write .{ext} images .{ext} 이미지는 쓸 수 없습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index b3388fa82a..f4db319711 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -3351,6 +3351,8 @@ theme Appearance Vormgeving Rename Library Group Bibliotheekgroep hernoemen Delete Library Group Bibliotheekgroep verwijderen Move Library Item to Group Bibliotheekitem naar groep verplaatsen + Rename Library Item Bibliotheekitem hernoemen + Add to Character Styles Aan tekenstijlen toevoegen Rename Library Bibliotheek hernoemen Show Library Bibliotheek weergeven Use Library Item Bibliotheekitem gebruiken @@ -4197,6 +4199,7 @@ imageTrace Outline Contour @plural {n} step|{n} steps {n} stap|{n} stappen @plural {n} stroke|{n} strokes {n} lijn|{n} lijnen @msg no library `{id}` geen bibliotheek `{id}` +@msg the library has another item of that kind named `{name}` de bibliotheek heeft al een ander item van die soort met de naam `{name}` @msg select some type first selecteer eerst wat tekst @msg kerning must be a number, "auto", or "optical" kerning moet een getal, ‘auto’ of ‘optical’ zijn @msg {_1} has no {_2} style {_1} heeft geen {_2}stijl diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 0729e35c79..770622cc1e 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3483,6 +3483,8 @@ theme Appearance Wygląd Rename Library Group Zmień nazwę grupy biblioteki Delete Library Group Usuń grupę biblioteki Move Library Item to Group Przenieś element biblioteki do grupy + Rename Library Item Zmień nazwę elementu biblioteki + Add to Character Styles Dodaj do stylów znakowych Rename Library Zmień nazwę biblioteki Show Library Pokaż bibliotekę Use Library Item Użyj elementu biblioteki @@ -4300,6 +4302,7 @@ imageTrace Outline Kontur @plural {n} step|{n} steps {n} krok|{n} kroki|{n} kroków @plural {n} stroke|{n} strokes {n} obrys|{n} obrysy|{n} obrysów @msg no library `{id}` brak biblioteki `{id}` +@msg the library has another item of that kind named `{name}` biblioteka ma już inny element tego rodzaju o nazwie `{name}` @msg select some type first najpierw zaznacz tekst @msg kerning must be a number, "auto", or "optical" kerning musi być liczbą, „auto” lub „optical” @msg the selection has no gradient fill zaznaczenie nie ma gradientu wypełnienia diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 36f778dbd5..77fce38c53 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3705,6 +3705,8 @@ font-technology Variable Variáveis Rename Library Group Renomear grupo de biblioteca Delete Library Group Excluir grupo de biblioteca Move Library Item to Group Mover item da biblioteca para o grupo + Rename Library Item Renomear item da biblioteca + Add to Character Styles Adicionar aos Estilos de Caractere Library Biblioteca Rename Library Renomear biblioteca Show Library Mostrar biblioteca @@ -3742,6 +3744,7 @@ axis Both Ambos Drag to float the toolbar Arraste para flutuar a barra de ferramentas Drag to the window's left or right edge to dock Arraste até a borda esquerda ou direita da janela para encaixar @msg no library `{id}` nenhuma biblioteca `{id}` +@msg the library has another item of that kind named `{name}` a biblioteca já tem outro item desse tipo chamado `{name}` @msg select some type first selecione algum texto primeiro @msg kerning must be a number, "auto", or "optical" O kerning deve ser um número, “auto” ou “optical” @msg the selection has no gradient fill a seleção não tem gradiente de preenchimento diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 442a9612d4..b546e87258 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3615,6 +3615,8 @@ digits Malayalam Малаялам Rename Library Group Переименовать группу библиотеки Delete Library Group Удалить группу библиотеки Move Library Item to Group Переместить элемент библиотеки в группу + Rename Library Item Переименовать элемент библиотеки + Add to Character Styles Добавить в стили символов Library Библиотека Rename Library Переименовать библиотеку Show Library Показать библиотеку @@ -4204,6 +4206,7 @@ axis Both Оба # (`untranslated_report` lists any that are missing). Command ids, parameter # names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are. @msg no library `{id}` нет библиотеки `{id}` +@msg the library has another item of that kind named `{name}` в библиотеке уже есть другой элемент этого вида с именем `{name}` @msg select some type first сначала выделите текст @msg kerning must be a number, "auto", or "optical" kerning должен быть числом, «auto» или «optical» @msg the selection has no gradient fill у выделения нет градиентной заливки diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 8a8f8453b6..9271ac4bac 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -3505,6 +3505,8 @@ digits Malayalam Malayalam Rename Library Group Byt namn på biblioteksgrupp Delete Library Group Radera biblioteksgrupp Move Library Item to Group Flytta biblioteksobjekt till grupp + Rename Library Item Byt namn på biblioteksobjekt + Add to Character Styles Lägg till i teckenstilar Library Bibliotek Rename Library Byt namn på bibliotek Show Library Visa bibliotek @@ -4078,6 +4080,7 @@ axis Both Båda Drag to the window's left or right edge to dock Dra till fönstrets vänster- eller högerkant för att docka Float Tool Group Gör verktygsgruppen flytande @msg no library `{id}` inget bibliotek `{id}` +@msg the library has another item of that kind named `{name}` biblioteket har redan ett annat objekt av den sorten med namnet `{name}` @msg select some type first markera någon text först @msg kerning must be a number, "auto", or "optical" kerning måste vara ett tal, ”auto” eller ”optical” @msg the selection has no gradient fill markeringen har ingen fyllnadsgradient diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index ef8773c31b..107f30f3ea 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3394,6 +3394,8 @@ digits Malayalam Malayalam Rename Library Group Kitaplık Grubunu Yeniden Adlandır Delete Library Group Kütüphane Grubunu Sil Move Library Item to Group Kitaplık Öğesini Gruba Taşı + Rename Library Item Kitaplık Öğesini Yeniden Adlandır + Add to Character Styles Karakter Stillerine Ekle Library Kütüphane Rename Library Kitaplığı Yeniden Adlandır Show Library Kitaplığı Göster @@ -3962,6 +3964,7 @@ axis Both Her İkisi Drag to the window's left or right edge to dock Sabitlemek için pencerenin sol veya sağ kenarına sürükleyin Float Tool Group Araç Grubunu Yüzer Yap @msg no library `{id}` `{id}` kütüphanesi yok +@msg the library has another item of that kind named `{name}` kitaplıkta zaten bu türden `{name}` adlı başka bir öğe var @msg select some type first önce bir tür seçin @msg kerning must be a number, "auto", or "optical" kerning bir sayı, “auto” veya “optical” olmalıdır @msg the selection has no gradient fill seçimin degrade dolgusu yok diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 0bccbd8cb6..68aabf0253 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3621,6 +3621,8 @@ effect Points Точки Rename Library Group Перейменувати групу бібліотеки Delete Library Group Видалити групу бібліотеки Move Library Item to Group Перемістити елемент бібліотеки в групу + Rename Library Item Перейменувати елемент бібліотеки + Add to Character Styles Додати до стилів символів Library Бібліотека Rename Library Перейменувати бібліотеку Show Library Показати бібліотеку @@ -4202,6 +4204,7 @@ axis Both Обидві Drag to the window's left or right edge to dock Перетягніть до лівого або правого краю вікна для прикріплення Float Tool Group Від’єднати групу інструментів @msg no library `{id}` немає бібліотеки `{id}` +@msg the library has another item of that kind named `{name}` у бібліотеці вже є інший елемент цього виду з назвою `{name}` @msg select some type first спочатку виділіть текст @msg kerning must be a number, "auto", or "optical" kerning має бути числом, «auto» або «optical» @msg the selection has no gradient fill виділення не має градієнтної заливки diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 4f084029db..36793e6c56 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2424,6 +2424,8 @@ digits Hindi Hindi Move Guide to Layer Di chuyển đường căn vào lớp Move in Perspective Di chuyển theo phối cảnh Move Library Item to Group Di chuyển mục thư viện vào nhóm + Rename Library Item Đổi tên mục thư viện + Add to Character Styles Thêm vào các kiểu ký tự Move Locked and Hidden Artwork with Artboard Di chuyển tác phẩm đã khóa và ẩn cùng bảng vẽ Move Mesh Point Di chuyển điểm lưới Move Mesh Points Di chuyển các điểm lưới @@ -4974,6 +4976,7 @@ theme Appearance Giao diện @msg no interaction in progress không có tương tác nào đang tiến hành @msg no key object không có đối tượng khóa @msg no library `{id}` không có thư viện `{id}` +@msg the library has another item of that kind named `{name}` thư viện đã có một mục khác cùng loại tên là `{name}` @msg no library `{key}` (see {list}) không có thư viện `{key}` (xem {list}) @msg No linked files to look for Không có tệp liên kết để tìm kiếm @msg no Live Paint edge near that point không có cạnh tô trực tiếp gần điểm đó diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index b04e65a65b..27cad80b4a 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -3444,6 +3444,8 @@ digits Malayalam 马拉雅拉姆数字 Rename Library Group 重命名库组 Delete Library Group 删除库组 Move Library Item to Group 将库项目移到组 + Rename Library Item 重命名库项目 + Add to Character Styles 添加到字符样式 Library 库 Rename Library 重命名库 Show Library 显示库 @@ -4195,6 +4197,7 @@ imageTrace Outline 轮廓 @msg the layer to flatten into is hidden 要拼合到的图层已隐藏 @msg there are no objects to release 没有可释放的对象 @msg no library `{id}` 没有库 `{id}` +@msg the library has another item of that kind named `{name}` 库中已有另一个同类项目名为 `{name}` @msg {path}: not a file {path}:不是文件 @msg {path}: {e} {path}:{e} @msg can't write .{ext} images 无法写入 .{ext} 图像 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index be62fde8a7..ab1fd359cd 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1959,6 +1959,7 @@ file-filter Templates 範本 @msg the layer to flatten into is hidden 要平面化到的圖層已隱藏 @msg there are no objects to release 沒有可釋放的物件 @msg no library `{id}` 沒有資料庫 `{id}` +@msg the library has another item of that kind named `{name}` 資料庫中已有另一個同類項目名為 `{name}` @msg {path}: not a file {path}:不是檔案 @msg {path}: {e} {path}:{e} @msg can't write .{ext} images 無法寫入 .{ext} 影像 @@ -4680,6 +4681,8 @@ digits Malayalam 馬來亞拉姆數字 Rename Library Group 重新命名資料庫群組 Delete Library Group 刪除資料庫群組 Move Library Item to Group 將資料庫項目移至群組 + Rename Library Item 重新命名資料庫項目 + Add to Character Styles 新增至字元樣式 Library 資料庫 Rename Library 重新命名資料庫 Show Library 顯示資料庫 diff --git a/crates/ui-egui/src/panels/libraries.rs b/crates/ui-egui/src/panels/libraries.rs index 78ce76da32..84f72e5bdc 100644 --- a/crates/ui-egui/src/panels/libraries.rs +++ b/crates/ui-egui/src/panels/libraries.rs @@ -4,8 +4,10 @@ //! patterns), are added to it; the + button adds the selection's graphic, fill or stroke color, //! pattern or gradient, or text styles. A click on a color, gradient or pattern paints the //! selection through the active Fill/Stroke proxy, a style click applies it to the selected type, -//! and a graphic is dragged onto the canvas (or double-clicked) to place a copy. Libraries are -//! kept on this machine. +//! and a graphic is dragged onto the canvas (or double-clicked) to place a copy. A character +//! style's context menu also applies it, adds it to the document's character styles without +//! applying it (`library.addToCharacterStyles`) and renames it (`library.renameItem`). Libraries +//! are kept on this machine. //! //! The search field filters the items by name; items can be moved into user-named groups, shown as //! collapsible sections after the ungrouped items; and the panel menu exports the library to a @@ -94,12 +96,22 @@ fn ask_group_name(app: &mut VectorcraftApp, library: &str, rename: Option<&str>, app.ui.dialog = Some(crate::state::Dialog::new("command", d)); } +/// The name dialog for renaming item `item` (its key) of `kind` in `library`, filled with its name. +fn ask_item_name(app: &mut VectorcraftApp, library: &str, kind: ItemKind, item: &str) { + let name = app.session.libraries.get(library).and_then(|l| l.items().find(|(k, key, _)| *k == kind && *key == item)).map(|(.., n)| n.to_string()); + let fixed = json!({ "library": library, "kind": kind_param(kind), "item": item }); + let d = json!({ "__command": "library.renameItem", "__label": "Rename Library Item", "name": name.unwrap_or_else(|| item.to_string()), "__params": fixed }); + app.ui.dialog = Some(crate::state::Dialog::new("command", d)); +} + /// What a click in the panel asks for, done once the list is drawn. enum Act { Run(&'static str, Value), /// The New Group dialog; the items (`[{kind, item}]`) go into the new group. NewGroup(Value), RenameGroup(String), + /// The Rename dialog for an item (its kind and key). + RenameItem(ItemKind, String), } /// What a drag in the panel holds for the library shown: one of its items, one of its groups (by @@ -164,6 +176,9 @@ struct Items<'a> { grab: Option, /// Whether the items show in a list (else colours and graphics as icons). list: bool, + /// Whether a document is open, and whether type is selected there for a style to apply to. + has_doc: bool, + can_apply: bool, } impl Items<'_> { @@ -214,7 +229,10 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let searching = !query.is_empty(); let shown = |name: &str| !searching || name.to_lowercase().contains(&query); let grab = egui::DragAndDrop::payload::(ui.ctx()).and_then(|d| grabbed(&d, &lib_id)); - let items = Items { library: &lib_id, lib, groups: lib.group_index(), fill_active, grab, list }; + // Whether a character style's Apply Character Style is enabled (looked up only when the library has one). + let can_apply = + has_doc && !lib.char_styles.is_empty() && vectorcraft_engine::cmd::textstyles::target_style_attrs(&app.session, &json!({}), false).is_some(); + let items = Items { library: &lib_id, lib, groups: lib.group_index(), fill_active, grab, list, has_doc, can_apply }; // The items shown in each section: the ungrouped ones first, then each group's. let mut counts = vec![0usize; lib.groups.len() + 1]; for (kind, key, _) in lib.items().filter(|(_, _, name)| shown(name)) { @@ -374,6 +392,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } Some(Act::NewGroup(items)) => ask_group_name(app, &lib_id, None, items), Some(Act::RenameGroup(g)) => ask_group_name(app, &lib_id, Some(&g), Value::Null), + Some(Act::RenameItem(kind, item)) => ask_item_name(app, &lib_id, kind, &item), None => {} } } @@ -466,7 +485,13 @@ impl Items<'_> { if resp.clicked() { *act = Some(Act::Run("library.use", json!({ "kind": kind_param(kind), "item": st.name }))); } - resp.context_menu(|ui| self.item_menu(ui, act, kind, &st.name)); + resp.context_menu(|ui| { + if kind == ItemKind::CharStyle { + self.char_style_menu(ui, act, &st.name); + } else { + self.item_menu(ui, act, kind, &st.name); + } + }); } ui.add_space(6.0); } @@ -597,6 +622,40 @@ impl Items<'_> { /// An item's context menu entries: Move to Group (No Group, the library's groups, New /// Group…) and Delete. fn item_menu(&self, ui: &mut Ui, act: &mut Option, kind: ItemKind, key: &str) { + self.group_menu(ui, act, kind, key); + if menu_item(ui, "Delete", true, false) { + *act = Some(Act::Run("library.removeItem", json!({ "kind": kind_param(kind), "item": key }))); + ui.close(); + } + } + + /// A character style's context menu: Apply Character Style (with type selected), Add to + /// Character Styles (the document's, not applied), Move to Group, Rename… and Delete. + fn char_style_menu(&self, ui: &mut Ui, act: &mut Option, key: &str) { + let item = json!({ "kind": "charStyle", "item": key }); + if menu_item(ui, "Apply Character Style", self.can_apply, false) { + *act = Some(Act::Run("library.use", item.clone())); + ui.close(); + } + if menu_item(ui, "Add to Character Styles", self.has_doc, false) { + *act = Some(Act::Run("library.addToCharacterStyles", json!({ "item": key }))); + ui.close(); + } + ui.separator(); + self.group_menu(ui, act, ItemKind::CharStyle, key); + if menu_item(ui, "Rename…", true, false) { + *act = Some(Act::RenameItem(ItemKind::CharStyle, key.to_string())); + ui.close(); + } + if menu_item(ui, "Delete", true, false) { + *act = Some(Act::Run("library.removeItem", item)); + ui.close(); + } + } + + /// The Move to Group submenu of an item's context menu: No Group, the library's groups and + /// New Group…. + fn group_menu(&self, ui: &mut Ui, act: &mut Option, kind: ItemKind, key: &str) { let at = self.group_of(kind, key); let item = json!({ "kind": kind_param(kind), "item": key }); ui.menu_button(format!(" {}", tl!("Move to Group")), |ui| { @@ -624,10 +683,6 @@ impl Items<'_> { } }); }); - if menu_item(ui, "Delete", true, false) { - *act = Some(Act::Run("library.removeItem", item)); - ui.close(); - } } } diff --git a/crates/ui-egui/src/panels/libraries/tests.rs b/crates/ui-egui/src/panels/libraries/tests.rs index 5f7f575c8f..931a91a444 100644 --- a/crates/ui-egui/src/panels/libraries/tests.rs +++ b/crates/ui-egui/src/panels/libraries/tests.rs @@ -416,6 +416,89 @@ fn group_dialogs_run_with_their_library_and_group() { assert_eq!(got["groups"], json!([{"name": "Cold", "items": [{"kind": "color", "item": "Blue"}]}])); } +/// A `button` click at `pos`, pressed in one frame and released in the next. +fn click_at(app: &mut VectorcraftApp, ctx: &egui::Context, pos: egui::Pos2, button: egui::PointerButton) -> Vec<(String, Rect)> { + let event = |pressed| egui::Event::PointerButton { pos, button, pressed, modifiers: Default::default() }; + frame_at(app, ctx, 600.0, vec![egui::Event::PointerMoved(pos), event(true)]); + frame_at(app, ctx, 600.0, vec![event(false)]); + frame_at(app, ctx, 600.0, vec![]).0 +} + +/// The menu entries shown in `texts`, in order from the top: the labels starting with the menu's +/// indent. +fn menu_entries(texts: &[(String, Rect)]) -> Vec { + let mut entries: Vec<_> = texts.iter().filter(|(t, _)| t.starts_with(" ")).map(|(t, r)| (t.trim().to_string(), r.top())).collect(); + entries.sort_by(|a, b| a.1.total_cmp(&b.1)); + entries.into_iter().map(|(t, _)| t).collect() +} + +/// A right-click on a library character style opens its menu without applying it: Apply +/// Character Style (only with type selected), Add to Character Styles, Move to Group, Rename… and +/// Delete. Add puts the style in the document's character styles and leaves the selected type as +/// it was; Rename… opens a dialog with the style's name that renames it on OK and changes nothing +/// on Cancel. A paragraph style's menu stays Move to Group and Delete. +#[test] +fn a_character_style_s_menu_applies_adds_renames_and_deletes() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let data = json!({"name": "Type", "charStyles": [{"name": "Headline", "attrs": {"size": 31.0}}], "paraStyles": [{"name": "Body", "attrs": {}}]}); + let lib = app.run("library.import", json!({"data": data.to_string()})).unwrap()["id"].as_str().unwrap().to_string(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + // No type selected: Apply is there but does nothing. + let menu = click_at(&mut app, &ctx, at(&texts, "Headline").center(), egui::PointerButton::Secondary); + assert_eq!(menu_entries(&menu), ["Apply Character Style", "Add to Character Styles", "Move to Group", "Rename…", "Delete"]); + click_at(&mut app, &ctx, at(&menu, " Apply Character Style").center(), egui::PointerButton::Primary); + assert!(app.session.active().unwrap().doc.char_styles.is_empty(), "disabled without type"); + // With type selected: a right-click applies nothing; Add adds the style, unapplied. + let t = app.run("text.create", json!({"x": 10, "y": 80, "text": "Type", "size": 9})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [t]})).unwrap(); + let style_of = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(t)).unwrap().kind { + vectorcraft_doc::NodeKind::Text(text) => (text.runs[0].style.size, text.runs[0].style.style_name.clone()), + _ => panic!("not text"), + }; + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let menu = click_at(&mut app, &ctx, at(&texts, "Headline").center(), egui::PointerButton::Secondary); + assert_eq!(style_of(&app), (9.0, None), "a right-click applies nothing"); + let undo = app.session.active().unwrap().history.undo.len(); + click_at(&mut app, &ctx, at(&menu, " Add to Character Styles").center(), egui::PointerButton::Primary); + let doc = &app.session.active().unwrap().doc; + assert_eq!(doc.char_styles.iter().map(|c| c.name.as_str()).collect::>(), ["Headline"]); + assert_eq!(style_of(&app), (9.0, None), "added, not applied"); + assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1); + // Apply applies it. + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let menu = click_at(&mut app, &ctx, at(&texts, "Headline").center(), egui::PointerButton::Secondary); + click_at(&mut app, &ctx, at(&menu, " Apply Character Style").center(), egui::PointerButton::Primary); + assert_eq!(style_of(&app), (31.0, Some("Headline".into()))); + // Rename…: Cancel keeps the name, OK renames. + for (ok, want) in [(false, "Headline"), (true, "Title")] { + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let menu = click_at(&mut app, &ctx, at(&texts, "Headline").center(), egui::PointerButton::Secondary); + click_at(&mut app, &ctx, at(&menu, " Rename…").center(), egui::PointerButton::Primary); + let d = app.ui.dialog.as_mut().expect("the Rename dialog"); + assert_eq!((d.str("__command"), d.str("name")), ("library.renameItem".to_string(), "Headline".to_string())); + d.fields.insert("name".into(), json!("Title")); + if ok { + crate::dialogs::confirm(&mut app).unwrap(); + } else { + crate::dialogs::cancel(&mut app); + } + assert_eq!(style_order(&mut app), [want]); + } + // The paragraph style's menu. + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let menu = click_at(&mut app, &ctx, at(&texts, "Body").center(), egui::PointerButton::Secondary); + assert_eq!(menu_entries(&menu), ["Move to Group", "Delete"]); + // Delete removes the character style from the library only. + let (texts, _) = frame_at(&mut app, &ctx, 600.0, vec![]); + let menu = click_at(&mut app, &ctx, at(&texts, "Title").center(), egui::PointerButton::Secondary); + click_at(&mut app, &ctx, at(&menu, " Delete").center(), egui::PointerButton::Primary); + assert!(app.session.libraries.get(&lib).unwrap().char_styles.is_empty()); + assert_eq!(app.session.active().unwrap().doc.char_styles.len(), 1, "the document's style stays"); +} + /// Export Library… writes the library's file and Import Library… adds it back as a new library. #[cfg(not(target_arch = "wasm32"))] #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 274db05a34..41e28969f4 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4051,7 +4051,15 @@ center?, to?}` uses one: a graphic is placed centred on `center` (pasted with it gives another colour merges into the document's), a colour paints the selection's fill or stroke, and a style is added to the document under its name without the white space around it (numbered when that name is taken by other attributes) and applied to the selected text, each as one undo step. `library.removeItem {library?, kind, item}` removes -one. +one. `library.addToCharacterStyles {library?, item}` adds a character style to the document's character styles without +applying it, so no text needs to be selected: a document style of that name with the same attributes is reused +(`existing: true`, no undo step), and a name taken by other attributes gets a number, as `library.use` names it; a new +style is one undo step. With either command, a character style's color linked to a swatch that is not a global or spot +swatch of the document keeps the color it shows without the link. `library.renameItem {library?, kind, item, name}` renames an item in place, its group keeping it +(a graphic keeps its id); the name is trimmed and has 1 to 256 characters, another item of its kind with that name is +an error, renaming an item to its own name writes nothing, and a rename is not a document undo step. A right-click on a +character style in the panel offers Apply Character Style (with type selected), Add to Character Styles, Move to Group, +Rename… and Delete ([help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/add-and-use-text-and-character-styles-in-libraries.html)). Gradients and patterns are kinds of their own (the panel's + button lists Fill Gradient, Stroke Gradient, Fill Pattern and Stroke Pattern, and a gradient or pattern swatch dragged from the Swatches panel onto the list is added). diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2da74aeeaf..291a5ada06 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1471,6 +1471,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Neutral wording:** labels, MCP tool text, docs and packaging use neutral names, and `cargo xtask brands` (part of `cargo xtask ci`) fails on vendor names. - **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize. The Libraries panel holds gradients and patterns: its + button adds the selection's fill or stroke gradient or pattern, a gradient or pattern swatch dragged from the Swatches panel is added, and a click paints the selection through the active Fill/Stroke proxy, a pattern bringing its definition and swatch into the document. A spot colour stays a spot colour: the library keeps its ink name and tint, and using it in any document links the paint to that document's spot swatch of that name, adding it when missing (#5812). + A right-click on a character style in the Libraries panel offers Apply Character Style, Add to Character Styles, Move to Group, Rename… and Delete ([help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/add-and-use-text-and-character-styles-in-libraries.html)): Apply works on the selected type, Add puts the style in the document's character styles without applying it (`library.addToCharacterStyles`), Rename… renames the library's style in place (`library.renameItem`) and Delete removes it from the library only. Opening the menu applies nothing. Tests: `tests::library::a_library_character_style_is_added_to_the_document_without_applying_it`, `library_items_are_renamed_in_place`, `panels::libraries::tests::a_character_style_s_menu_applies_adds_renames_and_deletes`. Object › Lock › All Artwork Above and Object › Hide › All Artwork Above deselect the selected objects they lock or hide, the key object included. Object › Hide › Unselected Artwork (Cmd/Ctrl+Alt+Shift+3, `object.hide.unselected`) hides, in one undo step, every object Select All takes that is not selected and holds no selected object; the members of a group that holds a selected object are hidden one by one, and Object › Show All shows them again. In isolation mode it hides only unselected objects inside the isolated object. Public reference: [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html), "Hide unselected artwork". Cmd/Ctrl+Alt+Shift+2 (`object.lock.unselected`) locks unselected artwork in one undo step and has no Object › Lock menu item. It locks unselected members of partly selected groups, clipping groups and compound paths, and unselected guides while Lock Guides is on. In ordinary isolation mode it locks only unselected art inside the isolated object. Object › Unlock All unlocks all locked art in the document, or inside the isolated object in isolation mode. The [public shortcut page](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) calls the action "Lock all deselected artwork".