diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index f5e488db..51a2e49c 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -1465,6 +1465,14 @@ impl Document { pub fn new_graphic_style_name(&self) -> String { (self.graphic_styles.len() + 1..).map(|i| format!("Graphic Style {i}")).find(|n| self.graphic_style(n).is_none()).unwrap_or_default() } + /// `base`, or the first free "`base` N" (N from 2) when a symbol already has that name. + pub fn unique_symbol_name(&self, base: &str) -> String { + let taken = |n: &str| self.symbols.iter().any(|s| s.name == n); + if !taken(base) { + return base.to_string(); + } + (2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string()) + } } fn default_graphic_styles() -> Vec { diff --git a/crates/engine/src/cmd/brushsym.rs b/crates/engine/src/cmd/brushsym.rs index 9249a8cf..df6358cf 100644 --- a/crates/engine/src/cmd/brushsym.rs +++ b/crates/engine/src/cmd/brushsym.rs @@ -526,14 +526,6 @@ fn normalize(mut art: Node) -> Result<(Node, (f64, f64), Point)> { Ok((art, (w, h), c)) } -fn unique_symbol_name(d: &Document, base: &str) -> String { - let taken = |n: &str| d.symbols.iter().any(|s| s.name == n); - if !taken(base) { - return base.to_string(); - } - (2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string()) -} - fn instances_of(d: &Document, name: Option<&str>) -> Vec { let mut v = vec![]; d.walk(|n| { @@ -587,7 +579,7 @@ fn symbol_new(s: &mut Session, p: &Value) -> Result { let ids = roots(s, p)?; let art = selection_art(s, p)?.ok_or_else(|| EngineError::Other("nothing selected".into()))?; let (art, size, c) = normalize(art)?; - let name = unique_symbol_name(&s.doc()?.doc, str_param(p, "name").filter(|n| !n.trim().is_empty()).unwrap_or("New Symbol")); + let name = s.doc()?.doc.unique_symbol_name(str_param(p, "name").filter(|n| !n.trim().is_empty()).unwrap_or("New Symbol")); let id = s.edit("New Symbol", |d, sel| { let art = d.reid(&art); d.symbols.push(Symbol { name: name.clone(), art: Arc::new(art) }); @@ -764,7 +756,7 @@ fn symbol_duplicate(s: &mut Session, p: &Value) -> Result { let name = name_param(p, C)?.to_string(); let d = &s.doc()?.doc; let sym = d.symbols.iter().find(|x| x.name == name).cloned().ok_or_else(|| bad(C, format!("no symbol named `{name}`")))?; - let new = unique_symbol_name(d, str_param(p, "newName").unwrap_or(&format!("{name} copy"))); + let new = d.unique_symbol_name(str_param(p, "newName").unwrap_or(&format!("{name} copy"))); let size = natural_size(d, &name); s.edit("Duplicate Symbol", |d, _| { let art = d.reid(&sym.art); diff --git a/crates/ui-egui/src/dialogs/mod.rs b/crates/ui-egui/src/dialogs/mod.rs index 101a29f2..a2f76848 100644 --- a/crates/ui-egui/src/dialogs/mod.rs +++ b/crates/ui-egui/src/dialogs/mod.rs @@ -84,6 +84,7 @@ pub mod spot_colors; pub(crate) mod svg_options; pub mod swatch_conflict; pub mod swatch_options; +pub mod symbol_options; pub mod symbolism_options; mod text_export; pub mod text_import; @@ -290,6 +291,7 @@ registry! { LiquifyOptions: [liquify::KIND] => liquify::SPEC, FreehandOptions: [freehand::KIND] => freehand::SPEC, FlareOptions: [flare_options::KIND] => flare_options::SPEC, + SymbolOptions: [symbol_options::KIND] => symbol_options::SPEC, SymbolismOptions: [symbolism_options::KIND] => symbolism_options::SPEC, PerspectiveGridPresets: [perspective_presets::KIND] => perspective_presets::SPEC, PerspectiveGridOptions: [perspective_options::KIND] => perspective_options::SPEC, diff --git a/crates/ui-egui/src/dialogs/symbol_options.rs b/crates/ui-egui/src/dialogs/symbol_options.rs new file mode 100644 index 00000000..c30dc66f --- /dev/null +++ b/crates/ui-egui/src/dialogs/symbol_options.rs @@ -0,0 +1,45 @@ +//! Symbol Options: the name of the symbol New Symbol makes from the selection (`symbol.new`), asked +//! before the symbol is made, as the Symbols panel's New Symbol button and menu item do. +//! +//! Fields: `name`. + +use serde_json::{Value, json}; + +use super::swatch_options::{grid, label}; +use super::{DialogSpec, form}; +use crate::VectorcraftApp; +use crate::state::Dialog; + +/// The dialog kind of Symbol Options. +pub const KIND: &str = "symbolOptions"; + +pub(super) const SPEC: DialogSpec = DialogSpec { heading: |_| tl!("Symbol Options").into(), body, confirm, min_width: 320.0, ..DialogSpec::FORM }; + +/// Open Symbol Options to name a new symbol made from the selection. +pub fn open(app: &mut VectorcraftApp) -> Result { + let st = app.session.active().ok_or("no document open")?; + if st.selection.is_empty() { + return Err("nothing selected".into()); + } + let name = st.doc.unique_symbol_name("New Symbol"); + app.ui.dialog = Some(Dialog::new(KIND, json!({ "name": name }))); + Ok(Value::Null) +} + +fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + grid(ui, |ui| { + label(ui, tl!("Name:")); + form::text(ui, d, "name", 190.0); + ui.end_row(); + }); + false +} + +/// OK: make the symbol from the selection with the name typed. +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let r = app.run("symbol.new", json!({ "name": d.str("name") })); + if r.is_ok() { + app.ui.dialog = None; + } + r +} diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 3c482992..38fb02b0 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -808,6 +808,8 @@ lighting Light Světlo File Info Dialog Dialogové okno Informace o souboru Go to Artboard Přejít na pracovní plochu Graphic Style Options… Možnosti grafického stylu… + Symbol Options Možnosti symbolu + Symbol Options… Možnosti symbolu… Home Domů Import Keyboard Shortcuts… Importovat klávesové zkratky… Justify all lines Do bloku včetně posledního řádku diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 105a8e0c..88802b6c 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1128,6 +1128,8 @@ Graphic Style Library Grafikstil-Bibliothek Graphic Style Options Grafikstiloptionen Graphic Style Options… Grafikstiloptionen… + Symbol Options Symboloptionen + Symbol Options… Symboloptionen… Graphic Styles Grafikstile Graphic Styles Options Optionen für Grafikstile Graphics Grafik diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 07451c6b..f17b182b 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1145,6 +1145,8 @@ Graphic Style Library Biblioteca de estilos gráficos Graphic Style Options Opciones de estilo gráfico Graphic Style Options… Opciones de estilo gráfico… + Symbol Options Opciones de símbolo + Symbol Options… Opciones de símbolo… Graphic Styles Estilos gráficos Graphic Styles Options Opciones de estilos gráficos Graphics Gráficos diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 5ca5d91a..a0f8de67 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1144,6 +1144,8 @@ Graphic Style Library Bibliothèque de styles graphiques Graphic Style Options Options de style graphique Graphic Style Options… Options de style graphique… + Symbol Options Options de symbole + Symbol Options… Options de symbole… Graphic Styles Styles graphiques Graphic Styles Options Options des styles graphiques Graphics Graphiques diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index c17af681..0587a798 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1470,6 +1470,8 @@ Graphic Style Library Grafikaistílus-könyvtár Graphic Style Options Grafikai stílus beállításai Graphic Style Options… Grafikai stílus beállításai… + Symbol Options Szimbólum beállításai + Symbol Options… Szimbólum beállításai… Graphic Styles Grafikai stílusok Graphic Styles (4) Grafikai stílusok (4) Graphic Styles Options Grafikai stílusok beállításai diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 7852c2e7..073ebe69 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1144,6 +1144,8 @@ Graphic Style Library Libreria stili grafici Graphic Style Options Opzioni stile grafico Graphic Style Options… Opzioni stile grafico… + Symbol Options Opzioni simbolo + Symbol Options… Opzioni simbolo… Graphic Styles Stili grafici Graphic Styles Options Opzioni stili grafici Graphics Grafica diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 0bddb667..6b630b8c 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1422,6 +1422,8 @@ Graphic Style Library グラフィックスタイルライブラリ Graphic Style Options グラフィックスタイルオプション Graphic Style Options… グラフィックスタイルオプション… + Symbol Options シンボルオプション + Symbol Options… シンボルオプション… Graphic Styles Options グラフィックスタイルオプション Graphics Processor グラフィックプロセッサー Graphics: {adapter} グラフィックス: {adapter} diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 7c3cb271..b63378f3 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1417,6 +1417,8 @@ Graphic Style Library 그래픽 스타일 라이브러리 Graphic Style Options 그래픽 스타일 옵션 Graphic Style Options… 그래픽 스타일 옵션… + Symbol Options 심볼 옵션 + Symbol Options… 심볼 옵션… Graphic Styles Options 그래픽 스타일 옵션 Graphics Processor 그래픽 프로세서 Graphics: {adapter} 그래픽: {adapter} diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index b544cc84..aead02fa 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -782,6 +782,8 @@ lighting Light Światło File Info Dialog Okno informacji o pliku Go to Artboard Przejdź do obszaru kompozycji Graphic Style Options… Opcje stylu graficznego… + Symbol Options Opcje symbolu + Symbol Options… Opcje symbolu… Home Strona startowa Import Keyboard Shortcuts… Importuj skróty klawiaturowe… Justify all lines Justuj wszystkie wiersze diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 73a42222..1667c76d 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -983,6 +983,8 @@ Graphic Style Library Biblioteca de Estilos Gráficos Graphic Style Options Opções de Estilo Gráfico Graphic Style Options… Opções de Estilo Gráfico… + Symbol Options Opções de Símbolo + Symbol Options… Opções de Símbolo… Graphic Styles Estilos Gráficos Graphic Styles Options Opções de Estilos Gráficos Graphics Processor Processador Gráfico diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 480b698e..87438525 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1142,6 +1142,8 @@ Graphic Style Library Библиотека графических стилей Graphic Style Options Параметры графического стиля Graphic Style Options… Параметры графического стиля… + Symbol Options Параметры символа + Symbol Options… Параметры символа… Graphic Styles Графические стили Graphic Styles Options Параметры графических стилей Graphics Графика diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 8bb09d7e..0c5ec5ed 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1138,6 +1138,8 @@ Graphic Style Library Бібліотека графічних стилів Graphic Style Options Параметри графічного стилю Graphic Style Options… Параметри графічного стилю… + Symbol Options Параметри символу + Symbol Options… Параметри символу… Graphic Styles Графічні стилі Graphic Styles Options Параметри графічних стилів Graphics Графіка diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 59c05473..c44e6f66 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1323,6 +1323,8 @@ Graphic Style Library Thư viện kiểu đồ họa Graphic Style Options Tùy chọn kiểu đồ họa Graphic Style Options… Tùy chọn kiểu đồ họa… + Symbol Options Tùy chọn ký hiệu + Symbol Options… Tùy chọn ký hiệu… Graphic Styles Kiểu đồ họa Graphic Styles Options Tùy chọn kiểu đồ họa Graphics Đồ họa diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2c98f685..31222538 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1095,6 +1095,8 @@ Graphic Style Library 图形样式库 Graphic Style Options 图形样式选项 Graphic Style Options… 图形样式选项… + Symbol Options 符号选项 + Symbol Options… 符号选项… Graphic Styles 图形样式 Graphic Styles Options 图形样式选项 Graphics 图形 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 6b9fc177..286e042e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1119,6 +1119,8 @@ Graphic Style Library 繪圖樣式資料庫 Graphic Style Options 繪圖樣式選項 Graphic Style Options… 繪圖樣式選項… + Symbol Options 符號選項 + Symbol Options… 符號選項… Graphic Styles 繪圖樣式 Graphic Styles Options 繪圖樣式選項 Graphics 圖形 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 51b70b45..fe43ad58 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -403,6 +403,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{name?} open Graphic Style Options (dialog `graphicStyleOptions`, field `name`): for style `name` OK renames it (graphicStyle.rename); without, OK makes a new style of that name from the selection (graphicStyle.new)", ), + ( + "ui.symbolOptions", + "Symbol Options…", + "", + "{} open Symbol Options for a new symbol made from the selection, what the Symbols panel's New Symbol opens (dialog `symbolOptions`, field `name`, default the first free \"New Symbol\"/\"New Symbol N\"); OK runs symbol.new {name}", + ), ( "tool.options", "Tool Options…", @@ -1290,6 +1296,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::dialogs::open_color_picker(app, p), "ui.graphicStyleOptions" => crate::dialogs::graphic_style_options::open(app, s("name").as_deref()), + "ui.symbolOptions" => crate::dialogs::symbol_options::open(app), "ui.brushOptions" => crate::dialogs::brush_options::open(app, s("name").as_deref()), "tool.options" => crate::toolbar::open_options(app, &s("tool").unwrap_or_default()), "ui.colorGuideOptions" => { diff --git a/crates/ui-egui/src/panels/symbols.rs b/crates/ui-egui/src/panels/symbols.rs index 981b5643..69ca4581 100644 --- a/crates/ui-egui/src/panels/symbols.rs +++ b/crates/ui-egui/src/panels/symbols.rs @@ -155,7 +155,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0)); if widgets::icon_button_enabled(ui, "dc-new-item", tl!("New Symbol"), false, has_sel, 24.0).clicked() { - app.run("symbol.new", json!({})).ok(); + // Alt skips Symbol Options: the symbol is made under the default name. + let cmd = if ui.input(|i| i.modifiers.alt) { "symbol.new" } else { "ui.symbolOptions" }; + app.run(cmd, json!({})).ok(); } if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Symbol"), false, sel.is_some(), 24.0).clicked() && let Some(n) = &sel @@ -171,7 +173,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let inst = has_instances_selected(app); let name = json!({"name": sel}); let items: [(&str, bool, &str, Value); 9] = [ - (tl!("New Symbol…"), has_sel, "symbol.new", json!({})), + (tl!("New Symbol…"), has_sel, "ui.symbolOptions", json!({})), (tl!("Redefine Symbol"), has_sel && sel.is_some(), "symbol.update", name.clone()), (tl!("Duplicate Symbol"), sel.is_some(), "symbol.duplicate", name.clone()), (tl!("Delete Symbol"), sel.is_some(), "symbol.delete", name.clone()), @@ -245,4 +247,37 @@ mod tests { frame(&ctx, &mut app, vec![egui::Event::PointerMoved(tile + vec2(80.0, 80.0))]); assert_eq!(egui::DragAndDrop::payload::(&ctx).as_deref(), Some(&PanelDrag::Symbol(names[0].clone()))); } + + /// One headless frame of the panel menu; returns its painted labels. + fn menu_frame(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec) -> Vec<(String, Rect)> { + let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(236.0, 400.0))), events, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| menu(app, ui)); + out.textures_delta.clear(); + crate::tests_removeanchors::shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::>()) + } + + #[test] + fn new_symbol_asks_for_symbol_options_before_making_the_symbol() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let labels = menu_frame(&ctx, &mut app, vec![]); + let item = labels.iter().find(|(t, _)| t.trim() == "New Symbol…").map(|(_, r)| r.center()).expect("New Symbol… in the menu"); + menu_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(item), button(item, true)]); + menu_frame(&ctx, &mut app, vec![button(item, false)]); + assert_eq!(app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("symbolOptions")); + assert!(symbols(&mut app).is_empty(), "no symbol before OK"); + assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "New Symbol"); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + // The next one is offered a name no symbol has, and OK keeps the name typed. + app.run("shape.rectangle", json!({"x": 60, "y": 10, "width": 30, "height": 20})).unwrap(); + app.run("ui.symbolOptions", json!({})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().str("name"), "New Symbol 2"); + app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Badge")); + crate::dialogs::confirm(&mut app).unwrap(); + assert_eq!(symbols(&mut app), ["New Symbol", "Badge"]); + } } diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 097d06ac..a7f015ca 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -114,6 +114,9 @@ dialog open); without `name` it names a new style made from the selection (`grap `ui.mergeGraphicStyles {names}` opens the same dialog to name the style `ui.dialog.confirm` merges from them (`graphicStyle.merge`). Deleting styles from the panel asks first with a `confirm` dialog (`graphicStyle.delete`). +Symbols: `ui.symbolOptions` opens the `symbolOptions` dialog (field `name`) that the Symbols panel's New Symbol asks +first; `ui.dialog.confirm` makes the symbol from the selection with that name (`symbol.new`). + Gradient panel: double-clicking a stop on the panel's slider opens the same `gradientStop` popover, with a `screen` field (`[x, y]`, screen points) in place of `x`/`y`. The panel's stop eyedropper selects the Eyedropper tool with the tool option `stop` (the tool to return to; `tool.setOption {key: "stop", value: "gradient"}`): its next click on art