From de78142dd83506d2891402503d337b884fc1b91e Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 18:12:58 -0800 Subject: [PATCH] Toolbars: Manage Toolbars renames, copies and deletes the toolbars made with New Toolbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Window › Toolbars › Manage Toolbars… and Manage Toolbars… in the All Tools drawer's menu open a dialog that lists the toolbars made with New Toolbar. A name typed for the selected toolbar renames it, + copies the selected toolbar or, with none selected, makes a new one, and the trash can deletes it (https://helpx.adobe.com/illustrator/using/tools.html, Manage toolbars). VectorCraft's own details: - A rename applies at Enter, when the text box loses focus, or at OK. The name follows New Toolbar's rules: not empty, at most 200 characters, and not another toolbar's in any case. - A copy is called " Copy" (then " Copy 2"), shows a little below the toolbar it copies and below earlier copies; a new toolbar is called "Toolbar N". - Each change applies at once; deleting also removes the groups torn off the toolbar. - When the typed name is refused, a click on +, the trash can or another toolbar is not applied, so the name can be corrected. When the selected toolbar is deleted elsewhere while the dialog is open, none is selected. - Renaming or deleting a toolbar closes the All Tools drawer and a group's flyout open on it; a change of case only leaves them open, and the Tools panel's flyouts and other toolbars' stay open. - Each change shows under Window › Toolbars and is kept with the workspace. New commands: window.toolbar.manage, window.toolbar.rename, window.toolbar.duplicate and window.toolbar.delete, with catalog rows for their labels in every language. Docs: control-protocol.md, gaps.md, target-app-parity.md, ui-parity.md. Part of #1759 --- crates/ui-egui/src/custom_toolbars.rs | 517 +++++++++++++++++++++++- crates/ui-egui/src/dialogs.rs | 1 + crates/ui-egui/src/edit_toolbar.rs | 16 +- crates/ui-egui/src/i18n/bg.tsv | 5 + crates/ui-egui/src/i18n/bs.tsv | 5 + crates/ui-egui/src/i18n/ca.tsv | 5 + crates/ui-egui/src/i18n/cs.tsv | 5 + crates/ui-egui/src/i18n/da.tsv | 5 + crates/ui-egui/src/i18n/de.tsv | 5 + crates/ui-egui/src/i18n/el.tsv | 5 + crates/ui-egui/src/i18n/es.tsv | 5 + crates/ui-egui/src/i18n/fi.tsv | 5 + crates/ui-egui/src/i18n/fr.tsv | 5 + crates/ui-egui/src/i18n/hu.tsv | 5 + crates/ui-egui/src/i18n/id.tsv | 5 + crates/ui-egui/src/i18n/it.tsv | 5 + crates/ui-egui/src/i18n/ja.tsv | 5 + crates/ui-egui/src/i18n/ka.tsv | 5 + crates/ui-egui/src/i18n/ko.tsv | 5 + crates/ui-egui/src/i18n/nl.tsv | 5 + crates/ui-egui/src/i18n/pl.tsv | 5 + crates/ui-egui/src/i18n/pt-br.tsv | 5 + crates/ui-egui/src/i18n/ru.tsv | 5 + crates/ui-egui/src/i18n/sv.tsv | 5 + crates/ui-egui/src/i18n/tr.tsv | 5 + crates/ui-egui/src/i18n/uk.tsv | 5 + crates/ui-egui/src/i18n/vi.tsv | 5 + crates/ui-egui/src/i18n/zh-hans.tsv | 5 + crates/ui-egui/src/i18n/zh-hant.tsv | 5 + crates/ui-egui/src/menus/ui_commands.rs | 24 ++ docs/control-protocol.md | 10 + docs/gaps.md | 3 +- docs/target-app-parity.md | 3 +- docs/ui-parity.md | 3 +- 34 files changed, 696 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/custom_toolbars.rs b/crates/ui-egui/src/custom_toolbars.rs index 1e459a7fb..fa2be21c0 100644 --- a/crates/ui-egui/src/custom_toolbars.rs +++ b/crates/ui-egui/src/custom_toolbars.rs @@ -4,13 +4,16 @@ //! `toolbar` param of its commands). Its title bar moves it, its × hides it and its double arrow //! shows its tools in one column or two. Each toolbar is listed under Window › Toolbars, where its //! item shows or hides it, and is kept with the workspace it was made in (`Workspace::toolbars`). +//! Window › Toolbars › Manage Toolbars (also in the drawer's menu) renames, copies and deletes them. //! //! Behavior from the public documentation: //! (Create and manage toolbars: New Toolbar //! from Window › Toolbars or the drawer's flyout menu, a name and OK make a blank toolbar, its Edit //! Toolbar button opens the drawer to add tools, and a toolbar stays in the workspace it is made in; //! a toolbar moves by its title bar, whose double arrow toggles one column or two and whose close -//! button hides it). +//! button hides it. Manage toolbars: Manage Toolbars from Window › Toolbars or the drawer's flyout +//! menu; a new name typed for the selected toolbar renames it, + copies the selected toolbar or, +//! with none selected, makes a new one, and Delete deletes it). use std::sync::atomic::Ordering; @@ -118,6 +121,97 @@ fn show_alone_after_tab(ui: &mut UiState, name: &str) { } } +/// Where the toolbar named `name` is in the list, whatever its case. +fn index(ui: &UiState, name: &str) -> Result { + let k = key(name); + ui.custom_toolbars.iter().position(|b| key(&b.name) == k).ok_or_else(|| format!("no toolbar named `{name}`")) +} + +/// The All Tools drawer beside the toolbar named `old`, and a group's flyout open on it (`ctx`), +/// close when it is deleted (`new` is `None`) or renamed; a new name that changes only the case +/// leaves them open on it. +fn close_popups(ui: &mut UiState, ctx: Option<&egui::Context>, old: &str, new: Option<&str>) { + let same = new.is_some_and(|n| key(n) == key(old)); + if drawer_target(ui).map(key) == Some(key(old)) { + if same { + ui.drawer_toolbar = new.map(String::from); + } else { + ui.tools_drawer = false; + ui.drawer_toolbar = None; + } + } + let Some(ctx) = ctx else { return }; + let id = crate::toolbar::flyout_toolbar_id(); + if ui.flyout.is_some() && ctx.data(|d| d.get_temp::>(id)).flatten().is_some_and(|t| key(&t) == key(old)) { + if same { + ctx.data_mut(|d| d.insert_temp(id, new.map(String::from))); + } else { + ui.flyout = None; + } + } +} + +/// Give the toolbar named `name` the name `to`; returns the new name. +pub(crate) fn rename(ui: &mut UiState, ctx: Option<&egui::Context>, name: &str, to: &str) -> Result { + let to = to.trim(); + if to.is_empty() { + return Err("toolbar name can't be empty".into()); + } + if to.chars().count() > MAX_NAME { + return Err(format!("a toolbar name has at most {MAX_NAME} characters")); + } + let k = index(ui, name)?; + if ui.custom_toolbars.iter().enumerate().any(|(i, b)| i != k && key(&b.name) == key(to)) { + return Err(format!("there is already a toolbar named `{to}`")); + } + let old = ui.custom_toolbars.get(k).map(|b| b.name.clone()).unwrap_or_default(); + close_popups(ui, ctx, &old, Some(to)); + if let Some(b) = ui.custom_toolbars.get_mut(k) { + b.name = to.to_string(); + } + Ok(to.to_string()) +} + +/// A copy of the toolbar named `name`, with its tools, shown a little below it (further down +/// while an earlier copy is there) and called +/// "`name` Copy" (then "`name` Copy 2"…); without `name`, a blank toolbar called "Toolbar N". +/// Returns the new toolbar's name. +pub(crate) fn duplicate(ui: &mut UiState, name: Option<&str>) -> Result { + let Some(name) = name else { + let name = (1..).map(|i| format!("Toolbar {i}")).find(|n| find(ui, n).is_none()).unwrap_or_default(); + return create(ui, &name); + }; + let k = index(ui, name)?; + if ui.custom_toolbars.len() >= MAX { + return Err(format!("a workspace keeps at most {MAX} toolbars")); + } + let Some(mut copy) = ui.custom_toolbars.get(k).cloned() else { return Err(format!("no toolbar named `{name}`")) }; + // A long name is shortened so that "Copy" and its number fit in MAX_NAME characters. + let stem: String = copy.name.chars().take(MAX_NAME - " Copy 64".len()).collect(); + let base = format!("{} Copy", stem.trim_end()); + copy.name = std::iter::once(base.clone()).chain((2..).map(|i| format!("{base} {i}"))).find(|n| find(ui, n).is_none()).unwrap_or_default(); + copy.shown = true; + let from = copy.pos; + let at = |n: f32| [from[0] + 16.0 * n, from[1] + 24.0 * n]; + let n = (1..=MAX + 1).map(|n| n as f32).find(|&n| ui.custom_toolbars.iter().all(|b| b.pos != at(n))).unwrap_or(1.0); + copy.pos = at(n); + for f in &mut copy.floating { + f.pos = [f.pos[0] + 16.0 * n, f.pos[1] + 24.0 * n]; + } + let name = copy.name.clone(); + ui.custom_toolbars.push(copy); + show_alone_after_tab(ui, &name); + Ok(name) +} + +/// Delete the toolbar named `name`, with the groups torn off it. +pub(crate) fn delete(ui: &mut UiState, ctx: Option<&egui::Context>, name: &str) -> Result<(), String> { + let k = index(ui, name)?; + close_popups(ui, ctx, name, None); + ui.custom_toolbars.remove(k); + Ok(()) +} + /// Keep named toolbars, each name once whatever its case and at most [`MAX_NAME`] characters, of /// known tools, each once in its toolbar, at a position that is a number, with one strip per group /// torn off them, and each group's tool chosen last only when the group holds it (a hand-edited @@ -190,6 +284,26 @@ pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option { + app.ui.dialog = Some(Dialog::new(MANAGE, json!({ "selected": "", "name": "" }))); + Ok(Value::Null) + } + "window.toolbar.rename" => (|| { + let name = p.get("name").and_then(Value::as_str).ok_or("name (a toolbar's name) is required")?; + let to = p.get("to").and_then(Value::as_str).ok_or("to (the new name) is required")?; + rename(&mut app.ui, app.ui_ctx.as_ref(), name, to).map(|name| json!({ "toolbar": name })) + })(), + "window.toolbar.duplicate" => (|| { + let name = match p.get("name").filter(|v| !v.is_null()) { + Some(v) => Some(v.as_str().ok_or("name must be a toolbar's name")?), + None => None, + }; + duplicate(&mut app.ui, name).map(|name| json!({ "toolbar": name })) + })(), + "window.toolbar.delete" => (|| { + let name = p.get("name").and_then(Value::as_str).ok_or("name (a toolbar's name) is required")?; + delete(&mut app.ui, app.ui_ctx.as_ref(), name).map(|_| Value::Null) + })(), "window.toolbar.list" => Ok(json!( app.ui .custom_toolbars @@ -250,6 +364,143 @@ pub(crate) fn show_dialog(app: &mut VectorcraftApp, ctx: &egui::Context) { } } +/// Manage Toolbars' dialog. +pub(crate) const MANAGE: &str = "manageToolbars"; + +/// The name typed in Manage Toolbars' text box becomes the selected toolbar's (a no-op while it is +/// the same). Returns true when the selected toolbar was deleted or renamed while the dialog was +/// open (a command, another workspace): none is selected then, and the name typed for it is dropped. +fn rename_selected(app: &mut VectorcraftApp) -> Result { + let Some(d) = app.ui.dialog.as_ref() else { return Ok(false) }; + let (sel, name) = (d.str("selected"), d.str("name")); + if !sel.is_empty() && find(&app.ui, &sel).is_none() { + if let Some(d) = app.ui.dialog.as_mut() { + d.fields.insert("selected".into(), json!("")); + d.fields.insert("name".into(), json!("")); + } + return Ok(true); + } + if sel.is_empty() || sel == name.trim() { + return Ok(false); + } + let new = app.run("window.toolbar.rename", json!({ "name": sel, "to": name }))?; + let new = new.get("toolbar").and_then(Value::as_str).unwrap_or_default().to_string(); + if let Some(d) = app.ui.dialog.as_mut() { + d.fields.insert("selected".into(), json!(new)); + d.fields.insert("name".into(), json!(new)); + } + Ok(false) +} + +/// OK in Manage Toolbars: the typed name is kept, and the dialog closes. +pub(crate) fn confirm_manage(app: &mut VectorcraftApp) -> Result { + rename_selected(app)?; + app.ui.dialog = None; + Ok(Value::Null) +} + +/// The Manage Toolbars dialog: the toolbars made with New Toolbar; the selected one's name in the +/// text box, where a new name renames it; + copies the selected toolbar (none selected: makes a +/// blank one), and the trash can deletes it. +pub(crate) fn show_manage(app: &mut VectorcraftApp, ctx: &egui::Context) { + let Some(mut d) = app.ui.dialog.clone() else { return }; + let t = Tokens::get(ctx); + let (mut ok, mut commit) = (false, false); + // A toolbar clicked in the list (none: the click left none selected), + (true) or the trash + // can (false): each applied after the name typed for the selected toolbar. + let mut pick: Option = None; + let mut action: Option = None; + crate::dialogs::modal::show(ctx, "Manage Toolbars", egui::Id::unique("dialog-manage-toolbars"), -40.0, 22, |ui| { + ui.set_width(320.0); + crate::dialogs::modal::heading(ui, tl!("Manage Toolbars")); + ui.add_space(12.0); + let sel = d.str("selected"); + egui::Frame::NONE.fill(t.input).stroke(egui::Stroke::new(1.0, t.input_border)).inner_margin(egui::Margin::same(4)).show(ui, |ui| { + ui.set_min_height(140.0); + ui.set_width(ui.available_width()); + egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| { + for b in &app.ui.custom_toolbars { + let on = !sel.is_empty() && key(&sel) == key(&b.name); + if ui.selectable_label(on, egui::RichText::new(&b.name).color(t.text)).clicked() { + // A click on the selected toolbar leaves none selected. + pick = Some(if on { String::new() } else { b.name.clone() }); + } + } + }); + }); + ui.add_space(8.0); + ui.horizontal(|ui| { + let mut name = d.str("name"); + let r = ui.add_enabled(!sel.is_empty(), egui::TextEdit::singleline(&mut name).desired_width(220.0)); + if r.changed() { + d.fields.insert("name".into(), json!(name)); + } + commit = r.lost_focus(); + let tip = if sel.is_empty() { tl!("New Toolbar") } else { tl!("Duplicate Toolbar") }; + if widgets::icon_button(ui, "plus", tip, false, 24.0).clicked() { + action = Some(true); + } + if widgets::icon_button_enabled(ui, "trash-2", tl!("Delete Toolbar"), false, !sel.is_empty(), 24.0).clicked() { + action = Some(false); + } + }); + ui.add_space(16.0); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if widgets::primary_button(ui, tl!("OK")).clicked() { + ok = true; + } + }); + }); + app.ui.dialog = Some(d); + if ok { + if let Err(e) = confirm_manage(app) { + app.status(e); + } + return; + } + manage_step(app, commit, pick, action); +} + +/// What the Manage Toolbars dialog does after a frame: the name typed for the selected toolbar is +/// applied when the text box loses focus, another toolbar is clicked (`pick`) or + (`Some(true)`) +/// or the trash can (`Some(false)`) is clicked. When that rename fails, the click is not applied, +/// so the name typed can be corrected; when the selected toolbar has gone meanwhile, + and the trash +/// can are not applied either. +fn manage_step(app: &mut VectorcraftApp, commit: bool, pick: Option, mut action: Option) { + if commit || pick.is_some() || action.is_some() { + match rename_selected(app) { + Err(e) => { + app.status(e); + return; + } + // The toolbar + or the trash can was meant for is gone. + Ok(true) => action = None, + Ok(false) => {} + } + } + let sel = app.ui.dialog.as_ref().map(|d| d.str("selected")).unwrap_or_default(); + let done = match action { + Some(true) => Some(app.run("window.toolbar.duplicate", if sel.is_empty() { json!({}) } else { json!({ "name": sel }) })), + Some(false) => Some(app.run("window.toolbar.delete", json!({ "name": sel }))), + None => None, + }; + // The new toolbar is selected, ready to be renamed; after Delete, none is. + let select = match done { + Some(Ok(r)) => Some(r.get("toolbar").and_then(Value::as_str).unwrap_or_default().to_string()), + Some(Err(e)) => { + app.status(e); + None + } + None => pick, + }; + if let Some(name) = select + && let Some(d) = app.ui.dialog.as_mut() + { + d.fields.insert("selected".into(), json!(name)); + d.fields.insert("name".into(), json!(name)); + } +} + // ---------- the menu ---------- crate::shortcut_editor::ui_mirror!(fn names_store() -> Vec<&'static str>); @@ -269,7 +520,7 @@ pub fn sync(ui: &UiState) { } /// Window › Toolbars: Advanced and Double Column, the toolbars made with New Toolbar (each checked -/// while it shows), and New Toolbar…. +/// while it shows), New Toolbar… and Manage Toolbars…. pub fn menu_items() -> Vec { use crate::menus::Item; let c = |label: &'static str, id: &'static str| Item::Cmd(label, id, Value::Null); @@ -280,6 +531,7 @@ pub fn menu_items() -> Vec { v.push(Item::Sep); } v.push(c("New Toolbar…", "window.toolbar.new")); + v.push(c("Manage Toolbars…", "window.toolbar.manage")); v } @@ -567,7 +819,8 @@ mod tests { #[test] fn a_new_toolbar_is_listed_under_window_toolbars() { let mut app = app(); - assert_eq!(toolbars_menu(&app).last().map(|(l, id, _)| (*l, *id)), Some(("New Toolbar…", "window.toolbar.new"))); + let last: Vec<_> = toolbars_menu(&app).iter().rev().take(2).map(|(l, id, _)| (*l, *id)).collect(); + assert_eq!(last, [("Manage Toolbars…", "window.toolbar.manage"), ("New Toolbar…", "window.toolbar.new")]); assert_eq!(app.run("window.toolbar.new", json!({"name": " Mine "})).unwrap(), json!({"toolbar": "Mine"})); let menu = toolbars_menu(&app); let item = menu.iter().find(|(l, ..)| *l == "Mine").expect("Mine is listed"); @@ -1060,6 +1313,264 @@ mod tests { assert!(!drawn(&ctx, area_id("Mine"), egui::Order::Middle)); } + fn listed(app: &VectorcraftApp) -> Vec<&'static str> { + toolbars_menu(app).into_iter().filter(|(_, id, _)| *id == "window.toolbar.custom").map(|(l, ..)| l).collect() + } + + /// Manage Toolbars renames, copies and deletes the toolbars made with New Toolbar: each change + /// shows under Window › Toolbars and stays in the workspace after switching away and back. + #[test] + fn manage_toolbars_renames_duplicates_and_deletes() { + let mut app = app(); + let menu = toolbars_menu(&app); + assert_eq!(menu.last().map(|(l, id, _)| (*l, *id)), Some(("Manage Toolbars…", "window.toolbar.manage"))); + app.run("window.workspace.new", json!({"name": "Drawing"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + app.run("toolbar.addTool", json!({"tool": "pen", "toolbar": "Mine"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + let back = |app: &mut VectorcraftApp| { + app.run("window.workspace", json!({"name": "Essentials"})).unwrap(); + app.run("window.workspace", json!({"name": "Drawing"})).unwrap(); + }; + // Rename. + assert_eq!(app.run("window.toolbar.rename", json!({"name": "mine", "to": " Pens "})).unwrap(), json!({"toolbar": "Pens"})); + assert_eq!(listed(&app), ["Pens", "Other"]); + back(&mut app); + assert_eq!(listed(&app), ["Pens", "Other"]); + assert_eq!(find(&app.ui, "Pens").unwrap().slots, [["pen"]]); + assert!(app.run("window.toolbar.rename", json!({"name": "Pens", "to": "other"})).unwrap_err().contains("already")); + assert!(app.run("window.toolbar.rename", json!({"name": "Pens", "to": " "})).unwrap_err().contains("empty")); + assert!(app.run("window.toolbar.rename", json!({"name": "Nope", "to": "X"})).unwrap_err().contains("Nope")); + // Only the case changes. + app.run("window.toolbar.rename", json!({"name": "Pens", "to": "PENS"})).unwrap(); + app.run("window.toolbar.rename", json!({"name": "pens", "to": "Pens"})).unwrap(); + // Duplicate: a copy with its tools, shown; without a name, a blank toolbar. + app.run("window.toolbar.custom", json!({"name": "Pens", "show": false})).unwrap(); + assert_eq!(app.run("window.toolbar.duplicate", json!({"name": "pens"})).unwrap(), json!({"toolbar": "Pens Copy"})); + assert_eq!(app.run("window.toolbar.duplicate", json!({"name": "Pens"})).unwrap(), json!({"toolbar": "Pens Copy 2"})); + assert_eq!(app.run("window.toolbar.duplicate", json!({})).unwrap(), json!({"toolbar": "Toolbar 1"})); + let copy = find(&app.ui, "Pens Copy").unwrap(); + assert!(copy.shown && copy.slots == [["pen"]]); + assert_ne!(copy.pos, find(&app.ui, "Pens").unwrap().pos, "not on top of the toolbar it copies"); + assert_ne!(copy.pos, find(&app.ui, "Pens Copy 2").unwrap().pos, "nor on top of an earlier copy"); + assert!(find(&app.ui, "Toolbar 1").unwrap().slots.is_empty()); + assert_eq!(listed(&app), ["Pens", "Other", "Pens Copy", "Pens Copy 2", "Toolbar 1"]); + // The copy is a toolbar of its own. + app.run("toolbar.addTool", json!({"tool": "rectangle", "toolbar": "Pens Copy"})).unwrap(); + assert_eq!(find(&app.ui, "Pens").unwrap().slots, [["pen"]]); + back(&mut app); + assert_eq!(listed(&app), ["Pens", "Other", "Pens Copy", "Pens Copy 2", "Toolbar 1"]); + assert!(app.run("window.toolbar.duplicate", json!({"name": "Nope"})).is_err()); + // Delete. + app.run("window.toolbar.delete", json!({"name": "pens copy 2"})).unwrap(); + app.run("window.toolbar.delete", json!({"name": "Toolbar 1"})).unwrap(); + assert_eq!(listed(&app), ["Pens", "Other", "Pens Copy"]); + back(&mut app); + assert_eq!(listed(&app), ["Pens", "Other", "Pens Copy"]); + assert!(app.run("window.toolbar.delete", json!({"name": "Toolbar 1"})).unwrap_err().contains("Toolbar 1")); + // Kept in the saved workspace and the preferences. + app.run("window.workspace.new", json!({"name": "Drawing"})).unwrap(); + let prefs: UiState = serde_json::from_slice(&serde_json::to_vec(&app.ui).unwrap()).unwrap(); + let mut ui = prefs.sanitized(); + crate::workspaces::switch(&mut ui, "Essentials").unwrap(); + crate::workspaces::switch(&mut ui, "Drawing").unwrap(); + assert_eq!(ui.custom_toolbars.iter().map(|b| b.name.as_str()).collect::>(), ["Pens", "Other", "Pens Copy"]); + } + + /// Renaming or deleting the toolbar the All Tools drawer edits closes the drawer; a new name + /// that changes only the case leaves it open. + #[test] + fn renaming_or_deleting_the_toolbar_being_edited_closes_the_drawer() { + let mut app = app(); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + app.run("window.editToolbar", json!({"toolbar": "Mine", "open": true})).unwrap(); + app.run("window.toolbar.rename", json!({"name": "Other", "to": "Second"})).unwrap(); + assert_eq!(drawer_target(&app.ui), Some("Mine"), "another toolbar's rename leaves it open"); + app.run("window.toolbar.rename", json!({"name": "Mine", "to": "MINE"})).unwrap(); + assert_eq!(drawer_target(&app.ui), Some("MINE"), "a change of case leaves it open"); + app.run("window.toolbar.rename", json!({"name": "MINE", "to": "Mine"})).unwrap(); + app.run("window.toolbar.rename", json!({"name": "Mine", "to": "Pens"})).unwrap(); + assert_eq!(drawer_target(&app.ui), None); + app.run("window.editToolbar", json!({"toolbar": "Pens", "open": true})).unwrap(); + app.run("window.toolbar.delete", json!({"name": "Pens"})).unwrap(); + assert!(!app.ui.tools_drawer && app.ui.drawer_toolbar.is_none()); + } + + /// The Manage Toolbars dialog: the name typed for the selected toolbar renames it at OK, and a + /// name another toolbar has keeps the dialog open. + #[test] + fn the_manage_toolbars_dialog_renames_the_selected_toolbar() { + let mut app = app(); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + assert_eq!(app.run("window.toolbar.manage", json!({})).unwrap(), Value::Null); + let d = app.ui.dialog.as_mut().unwrap(); + assert_eq!(d.kind, MANAGE); + d.fields.insert("selected".into(), json!("Mine")); + d.fields.insert("name".into(), json!("Pens")); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + assert!(find(&app.ui, "Pens").is_some() && find(&app.ui, "Mine").is_none()); + // A name taken by another toolbar keeps the dialog open. + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + app.run("window.toolbar.manage", json!({})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("selected".into(), json!("Pens")); + d.fields.insert("name".into(), json!("other")); + assert!(crate::dialogs::confirm(&mut app).unwrap_err().contains("already")); + assert!(app.ui.dialog.is_some()); + // Drawn with no toolbar selected, the dialog stays open. + let ctx = egui::Context::default(); + app.ui.dialog.as_mut().unwrap().fields.insert("selected".into(), json!("")); + for k in 0..3 { + window_frame(&mut app, &ctx, f64::from(k) * 0.05, 800.0); + } + assert!(app.ui.dialog.as_ref().is_some_and(|d| d.kind == MANAGE)); + } + + /// An open flyout of a group on a toolbar closes when that toolbar is renamed or deleted; the + /// flyout of another toolbar or of the Tools panel stays open. + #[test] + fn renaming_or_deleting_a_toolbar_closes_only_its_own_flyout() { + let mut app = app(); + app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); + let ctx = egui::Context::default(); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + let mut time = 0.0; + let mut frames = |app: &mut VectorcraftApp| { + for _ in 0..2 { + time += 0.05; + window_frame(app, &ctx, time, 800.0); + } + }; + frames(&mut app); + let open = |app: &mut VectorcraftApp, toolbar: Option<&str>| { + app.ui.flyout = Some(0); + ctx.data_mut(|d| { + d.insert_temp(egui::Id::unique("flyout-tools"), vec!["rectangle".to_string(), "ellipse".to_string()]); + d.insert_temp(crate::toolbar::flyout_toolbar_id(), toolbar.map(String::from)); + }); + }; + for (toolbar, change) in [ + (None, json!({"name": "Other", "to": "Second"})), + (Some("Mine"), json!({"name": "Second", "to": "Third"})), + (Some("Mine"), json!({"name": "Mine", "to": "mine"})), + ] { + open(&mut app, toolbar); + app.run("window.toolbar.rename", change).unwrap(); + frames(&mut app); + assert_eq!(app.ui.flyout, Some(0), "{toolbar:?}'s flyout stays open"); + } + open(&mut app, Some("mine")); + app.run("window.toolbar.rename", json!({"name": "mine", "to": "Pens"})).unwrap(); + frames(&mut app); + assert_eq!(app.ui.flyout, None, "the renamed toolbar's flyout closes"); + open(&mut app, Some("Pens")); + app.run("window.toolbar.delete", json!({"name": "Third"})).unwrap(); + frames(&mut app); + assert_eq!(app.ui.flyout, Some(0), "another toolbar's delete leaves it open"); + app.run("window.toolbar.delete", json!({"name": "Pens"})).unwrap(); + frames(&mut app); + assert_eq!(app.ui.flyout, None, "the deleted toolbar's flyout closes"); + // Another toolbar that takes the old name before the next frame doesn't keep it open. + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + frames(&mut app); + open(&mut app, Some("Mine")); + app.run("window.toolbar.rename", json!({"name": "Mine", "to": "X"})).unwrap(); + app.run("window.toolbar.rename", json!({"name": "Other", "to": "Mine"})).unwrap(); + assert_eq!(app.ui.flyout, None, "renamed, then its name taken"); + open(&mut app, Some("Mine")); + app.run("window.toolbar.delete", json!({"name": "Mine"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + assert_eq!(app.ui.flyout, None, "deleted, then its name taken"); + } + + /// A toolbar deleted while Manage Toolbars has it selected leaves none selected, and a click + /// on + or the trash can meant for it does nothing, so the dialog can go on. + #[test] + fn manage_toolbars_lets_go_of_a_toolbar_deleted_elsewhere() { + let mut app = app(); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + for action in [Some(false), Some(true), None] { + app.run("window.toolbar.manage", json!({})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("selected".into(), json!("Mine")); + d.fields.insert("name".into(), json!("Typed")); + app.run("window.toolbar.delete", json!({"name": "Mine"})).unwrap(); + manage_step(&mut app, action.is_none(), None, action); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((d.str("selected"), d.str("name")), (String::new(), String::new()), "{action:?}"); + assert_eq!(listed(&app), ["Other"], "{action:?}"); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + app.run("window.toolbar.delete", json!({"name": "Other"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + } + // OK closes the dialog. + app.ui.dialog.as_mut().unwrap().fields.insert("selected".into(), json!("Gone")); + crate::dialogs::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + } + + /// A name that can't be given to the selected toolbar stops a click on +, the trash can or + /// another toolbar, so the name typed stays to be corrected. + #[test] + fn a_refused_name_stops_the_next_click_in_manage_toolbars() { + let mut app = app(); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + app.run("window.toolbar.manage", json!({})).unwrap(); + let typed = |app: &mut VectorcraftApp| { + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("selected".into(), json!("Mine")); + d.fields.insert("name".into(), json!("other")); + }; + for (pick, action) in [(None, Some(false)), (None, Some(true)), (Some("Other".to_string()), None)] { + typed(&mut app); + manage_step(&mut app, false, pick, action); + assert_eq!(listed(&app), ["Mine", "Other"]); + let d = app.ui.dialog.as_ref().unwrap(); + assert_eq!((d.str("selected"), d.str("name")), ("Mine".to_string(), "other".to_string())); + } + // A name that is free renames the toolbar first, then the trash can deletes it. + app.ui.dialog.as_mut().unwrap().fields.insert("name".into(), json!("Pens")); + manage_step(&mut app, false, None, Some(false)); + assert_eq!(listed(&app), ["Other"]); + } + + /// A new name and a copy's name keep to the length New Toolbar allows, so the toolbar is kept + /// when the preferences load again. + #[test] + fn renamed_and_copied_toolbars_keep_names_short_enough_to_load() { + let mut app = app(); + let long = "x".repeat(MAX_NAME); + app.run("window.toolbar.new", json!({"name": long})).unwrap(); + assert!(app.run("window.toolbar.rename", json!({"name": long, "to": "y".repeat(MAX_NAME + 1)})).unwrap_err().contains("at most")); + let copy = app.run("window.toolbar.duplicate", json!({"name": long})).unwrap(); + let copy = copy["toolbar"].as_str().unwrap().to_string(); + assert!(copy.ends_with(" Copy") && copy.chars().count() <= MAX_NAME, "{copy}"); + let second = app.run("window.toolbar.duplicate", json!({"name": long})).unwrap(); + assert!(second["toolbar"].as_str().unwrap().ends_with(" Copy 2")); + let mut bars = app.ui.custom_toolbars.clone(); + sanitize(&mut bars); + assert_eq!(bars.len(), 3); + } + + /// A copy made while Tab hides the toolbars shows alone, as a new toolbar does. + #[test] + fn a_copy_made_while_tab_hides_the_toolbars_shows_alone() { + let mut app = app(); + app.run("window.toolbar.new", json!({"name": "Mine"})).unwrap(); + app.run("window.toolbar.new", json!({"name": "Other"})).unwrap(); + app.ui.tab_hid = Some(Default::default()); + app.run("window.toolbar.duplicate", json!({"name": "Mine"})).unwrap(); + assert!(app.ui.tab_hid.is_none()); + let shown: Vec<_> = app.ui.custom_toolbars.iter().filter(|b| b.shown).map(|b| b.name.as_str()).collect(); + assert_eq!(shown, ["Mine Copy"]); + } + /// A hand-edited file: unnamed toolbars and repeated names go, unknown tools go, each tool once, /// and a group shows a tool chosen last only when the group holds it. #[test] diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index d68217e52..09c4b8c4e 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -298,6 +298,7 @@ registry! { Shortcuts: ["shortcuts"] => DialogSpec::window(crate::shortcut_editor::show, |app, _| crate::shortcut_editor::confirm(app)), Workspaces: ["newWorkspace", "manageWorkspaces"] => DialogSpec::window(crate::workspaces::show, |app, _| crate::workspaces::confirm(app)), NewToolbar: [crate::custom_toolbars::KIND] => DialogSpec::window(crate::custom_toolbars::show_dialog, |app, _| crate::custom_toolbars::confirm(app)), + ManageToolbars: [crate::custom_toolbars::MANAGE] => DialogSpec::window(crate::custom_toolbars::show_manage, |app, _| crate::custom_toolbars::confirm_manage(app)), FindFont: ["findFont"] => DialogSpec::window(crate::find_font::show, |app, _| crate::find_font::confirm(app)), SwatchOptions: [swatch_options::KIND] => swatch_options::SPEC, Confirm: [confirm::KIND] => confirm::SPEC, diff --git a/crates/ui-egui/src/edit_toolbar.rs b/crates/ui-egui/src/edit_toolbar.rs index 572067537..eb79cf357 100644 --- a/crates/ui-egui/src/edit_toolbar.rs +++ b/crates/ui-egui/src/edit_toolbar.rs @@ -481,15 +481,16 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) { let removing = dragged.is_some_and(|d| d.from_toolbar) && ctx.memory(|m| m.area_rect(id)).is_some_and(over); let mut picked = None; let mut reset = false; - let mut new_toolbar = false; + // New Toolbar… or Manage Toolbars… in the drawer's menu. + let mut toolbars_cmd = None; let mut advanced = None; let mut show_toggle = None; let resp = egui::Area::new(id).order(egui::Order::Foreground).fixed_pos(pos2(x, y)).show(ctx, |ui| { crate::toolbar::flyout_frame(&t, removing).inner_margin(egui::Margin::same(8)).show(ui, |ui| { let width = WIDTH.min(screen.width() - 2.0 * GAP - 16.0).max(160.0); ui.set_width(width); - // All Tools, and the drawer's flyout menu: Reset, the Basic or Advanced toolbar, and New - // Toolbar. + // All Tools, and the drawer's flyout menu: Reset, the Basic or Advanced toolbar, New + // Toolbar and Manage Toolbars. ui.horizontal(|ui| { ui.label(egui::RichText::new(tl!("All Tools")).size(13.0).color(t.text_strong)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { @@ -510,7 +511,10 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) { } ui.separator(); if widgets::menu_item(ui, "New Toolbar…", true, false) { - new_toolbar = true; + toolbars_cmd = Some("window.toolbar.new"); + } + if widgets::menu_item(ui, "Manage Toolbars…", true, false) { + toolbars_cmd = Some("window.toolbar.manage"); } }); }); @@ -571,7 +575,9 @@ pub(crate) fn drawer(app: &mut VectorcraftApp, ctx: &egui::Context) { if reset && let Err(e) = app.run("toolbar.reset", json!({ "toolbar": target })) { app.ui.status = e; } - if new_toolbar && let Err(e) = app.run("window.toolbar.new", json!({})) { + if let Some(id) = toolbars_cmd + && let Err(e) = app.run(id, json!({})) + { app.ui.status = e; } if let Some(advanced) = advanced diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 576b657ec..e0b464671 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2040,6 +2040,11 @@ font-technology Variable Променливи New Toolbar Нова лента с инструменти Show Toolbar Показване на лента с инструменти List Toolbars Списък на лентите с инструменти + Manage Toolbars… Управление на лентите с инструменти… + Manage Toolbars Управление на лентите с инструменти + Rename Toolbar Преименуване на лентата с инструменти + Duplicate Toolbar Дублиране на лентата с инструменти + Delete Toolbar Изтриване на лентата с инструменти New {kind} Style… Нов стил ({kind})… New… Нов… Next Следващ diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index fa9904149..0a730a8bb 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -645,6 +645,11 @@ New Toolbar Nova traka s alatima Show Toolbar Prikaži traku s alatima List Toolbars Izlistaj trake s alatima + Manage Toolbars… Upravljaj trakama s alatima… + Manage Toolbars Upravljaj trakama s alatima + Rename Toolbar Preimenuj traku s alatima + Duplicate Toolbar Dupliciraj traku s alatima + Delete Toolbar Izbriši traku s alatima Manage Workspaces… Upravljaj radnim prostorima… Control Kontrola Contextual Task Bar Kontekstna traka zadataka diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 0e21ff719..c54c8b11d 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2565,6 +2565,11 @@ digits Malayalam Malaiàlam New Toolbar Barra d’eines nova Show Toolbar Mostra la barra d’eines List Toolbars Llista les barres d’eines + Manage Toolbars… Gestiona les barres d’eines… + Manage Toolbars Gestiona les barres d’eines + Rename Toolbar Canvia el nom de la barra d’eines + Duplicate Toolbar Duplica la barra d’eines + Delete Toolbar Suprimeix la barra d’eines New {kind} Style… Nou estil de {kind}… New… Nou… Next Següent diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index bc3ccd20b..3e535f31d 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -653,6 +653,11 @@ New Toolbar Nový panel nástrojů Show Toolbar Zobrazit panel nástrojů List Toolbars Seznam panelů nástrojů + Manage Toolbars… Spravovat panely nástrojů… + Manage Toolbars Spravovat panely nástrojů + Rename Toolbar Přejmenovat panel nástrojů + Duplicate Toolbar Duplikovat panel nástrojů + Delete Toolbar Odstranit panel nástrojů Manage Workspaces… Spravovat pracovní prostory… Control Ovládací panel Contextual Task Bar Kontextový panel úloh diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 4a2c248dc..dd3b54a4e 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -603,6 +603,11 @@ New Toolbar Ny værktøjslinje Show Toolbar Vis værktøjslinje List Toolbars Liste over værktøjslinjer + Manage Toolbars… Administrér værktøjslinjer… + Manage Toolbars Administrér værktøjslinjer + Rename Toolbar Omdøb værktøjslinje + Duplicate Toolbar Duplikér værktøjslinje + Delete Toolbar Slet værktøjslinje Manage Workspaces… Administrér arbejdsområder… Control Kontrol Contextual Task Bar Kontekstuel opgavelinje diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index c9955c72b..4956d08d4 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2060,6 +2060,11 @@ file-filter Templates Vorlagen New Toolbar Neue Werkzeugleiste Show Toolbar Werkzeugleiste einblenden List Toolbars Werkzeugleisten auflisten + Manage Toolbars… Werkzeugleisten verwalten… + Manage Toolbars Werkzeugleisten verwalten + Rename Toolbar Werkzeugleiste umbenennen + Duplicate Toolbar Werkzeugleiste duplizieren + Delete Toolbar Werkzeugleiste löschen New {kind} Style… Neues {kind}format… New… Neu… Next Weiter diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 67b9a7317..d73cddb44 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1725,6 +1725,11 @@ font-technology Variable Μεταβλητές New Toolbar Νέα γραμμή εργαλείων Show Toolbar Εμφάνιση γραμμής εργαλείων List Toolbars Λίστα γραμμών εργαλείων + Manage Toolbars… Διαχείριση γραμμών εργαλείων… + Manage Toolbars Διαχείριση γραμμών εργαλείων + Rename Toolbar Μετονομασία γραμμής εργαλείων + Duplicate Toolbar Δημιουργία αντιγράφου γραμμής εργαλείων + Delete Toolbar Διαγραφή γραμμής εργαλείων New… Νέο… Next Επόμενο No document Δεν υπάρχει έγγραφο diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 09ec906bc..b43199c7e 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2107,6 +2107,11 @@ font-technology Variable Variables New Toolbar Nueva barra de herramientas Show Toolbar Mostrar barra de herramientas List Toolbars Lista de barras de herramientas + Manage Toolbars… Administrar barras de herramientas… + Manage Toolbars Administrar barras de herramientas + Rename Toolbar Cambiar nombre de la barra de herramientas + Duplicate Toolbar Duplicar barra de herramientas + Delete Toolbar Eliminar barra de herramientas New {kind} Style… Nuevo estilo de {kind}… New… Nuevo… Next Siguiente diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index ad10d0875..b211b4c48 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -644,6 +644,11 @@ New Toolbar Uusi työkalurivi Show Toolbar Näytä työkalurivi List Toolbars Luettele työkalurivit + Manage Toolbars… Hallitse työkalurivejä… + Manage Toolbars Hallitse työkalurivejä + Rename Toolbar Nimeä työkalurivi uudelleen + Duplicate Toolbar Monista työkalurivi + Delete Toolbar Poista työkalurivi Manage Workspaces… Hallitse työtiloja… Control Ohjaus Contextual Task Bar Kontekstuaalinen tehtäväpalkki diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 18dffaeb4..d80d26230 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2099,6 +2099,11 @@ font-technology Variable Variables New Toolbar Nouvelle barre d’outils Show Toolbar Afficher la barre d’outils List Toolbars Liste des barres d’outils + Manage Toolbars… Gérer les barres d’outils… + Manage Toolbars Gérer les barres d’outils + Rename Toolbar Renommer la barre d’outils + Duplicate Toolbar Dupliquer la barre d’outils + Delete Toolbar Supprimer la barre d’outils New {kind} Style… Nouveau style de {kind}… New… Nouveau… Next Suivant diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index fb1f9c7bc..5e5fae204 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2562,6 +2562,11 @@ file-filter Templates Sablonok New Toolbar Új eszköztár Show Toolbar Eszköztár megjelenítése List Toolbars Eszköztárak listája + Manage Toolbars… Eszköztárak kezelése… + Manage Toolbars Eszköztárak kezelése + Rename Toolbar Eszköztár átnevezése + Duplicate Toolbar Eszköztár kettőzése + Delete Toolbar Eszköztár törlése New {kind} Style… Új stílus: {kind}… New… Új… Next Következő diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index abf75d965..987d8db0d 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -603,6 +603,11 @@ New Toolbar Bilah alat baru Show Toolbar Tampilkan bilah alat List Toolbars Daftar bilah alat + Manage Toolbars… Kelola bilah alat… + Manage Toolbars Kelola bilah alat + Rename Toolbar Ganti nama bilah alat + Duplicate Toolbar Duplikatkan bilah alat + Delete Toolbar Hapus bilah alat Manage Workspaces… Kelola ruang kerja… Control Kontrol Contextual Task Bar Bilah tugas kontekstual diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 9d61d8c3e..1f43f530b 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2136,6 +2136,11 @@ file-filter Templates Modelli New Toolbar Nuova barra degli strumenti Show Toolbar Mostra barra degli strumenti List Toolbars Elenco barre degli strumenti + Manage Toolbars… Gestisci barre degli strumenti… + Manage Toolbars Gestisci barre degli strumenti + Rename Toolbar Rinomina barra degli strumenti + Duplicate Toolbar Duplica barra degli strumenti + Delete Toolbar Elimina barra degli strumenti New {kind} Style… Nuovo stile {kind}… New… Nuovo… Next Successivo diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 50241341d..6c3b72869 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -658,6 +658,11 @@ digits Malayalam マラヤーラム数字 New Toolbar 新規ツールバー Show Toolbar ツールバーを表示 List Toolbars ツールバーの一覧 + Manage Toolbars… ツールバーを管理… + Manage Toolbars ツールバーを管理 + Rename Toolbar ツールバーの名前を変更 + Duplicate Toolbar ツールバーを複製 + Delete Toolbar ツールバーを削除 Manage Workspaces… ワークスペースを管理… Control コントロール Contextual Task Bar コンテキストタスクバー diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 112dfa657..f85b3bcd5 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -547,6 +547,11 @@ New Toolbar ახალი ხელსაწყოთა ზოლი Show Toolbar ხელსაწყოთა ზოლის ჩვენება List Toolbars ხელსაწყოთა ზოლების სია + Manage Toolbars… ხელსაწყოთა ზოლების მართვა… + Manage Toolbars ხელსაწყოთა ზოლების მართვა + Rename Toolbar ხელსაწყოთა ზოლის სახელის შეცვლა + Duplicate Toolbar ხელსაწყოთა ზოლის დუბლირება + Delete Toolbar ხელსაწყოთა ზოლის წაშლა Manage Workspaces… სამუშაო სივრცეების მართვა… Control მართვა Contextual Task Bar კონტექსტური ამოცანების ზოლი diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 4ad48aa61..7a83c0316 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -651,6 +651,11 @@ digits Malayalam 말라얄람어 New Toolbar 새 도구 막대 Show Toolbar 도구 막대 표시 List Toolbars 도구 막대 목록 + Manage Toolbars… 도구 막대 관리… + Manage Toolbars 도구 막대 관리 + Rename Toolbar 도구 막대 이름 바꾸기 + Duplicate Toolbar 도구 막대 복제 + Delete Toolbar 도구 막대 삭제 Manage Workspaces… 작업 영역 관리… Control 컨트롤 Contextual Task Bar 상황별 작업 표시줄 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 5096aa81c..8d1deb504 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -617,6 +617,11 @@ New Toolbar Nieuwe werkbalk Show Toolbar Werkbalk tonen List Toolbars Lijst met werkbalken + Manage Toolbars… Werkbalken beheren… + Manage Toolbars Werkbalken beheren + Rename Toolbar Werkbalk hernoemen + Duplicate Toolbar Werkbalk dupliceren + Delete Toolbar Werkbalk verwijderen Manage Workspaces… Werkruimten beheren… Control Bediening Contextual Task Bar Contextuele taakbalk diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index d431ce0fb..c3ba69653 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -603,6 +603,11 @@ New Toolbar Nowy pasek narzędzi Show Toolbar Pokaż pasek narzędzi List Toolbars Lista pasków narzędzi + Manage Toolbars… Zarządzaj paskami narzędzi… + Manage Toolbars Zarządzaj paskami narzędzi + Rename Toolbar Zmień nazwę paska narzędzi + Duplicate Toolbar Powiel pasek narzędzi + Delete Toolbar Usuń pasek narzędzi Manage Workspaces… Zarządzaj obszarami roboczymi… Control Sterowanie Contextual Task Bar Kontekstowy pasek zadań diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index b726d9ddb..e0af75dda 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1724,6 +1724,11 @@ New Toolbar Nova Barra de Ferramentas Show Toolbar Mostrar Barra de Ferramentas List Toolbars Listar Barras de Ferramentas + Manage Toolbars… Gerenciar Barras de Ferramentas… + Manage Toolbars Gerenciar Barras de Ferramentas + Rename Toolbar Renomear Barra de Ferramentas + Duplicate Toolbar Duplicar Barra de Ferramentas + Delete Toolbar Excluir Barra de Ferramentas New file Novo arquivo New from Template Novo a partir de Modelo New from Template… Novo a partir de Modelo… diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 0cb2785ac..b37b9a319 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2131,6 +2131,11 @@ file-filter Templates Шаблоны New Toolbar Новая панель инструментов Show Toolbar Показать панель инструментов List Toolbars Список панелей инструментов + Manage Toolbars… Управление панелями инструментов… + Manage Toolbars Управление панелями инструментов + Rename Toolbar Переименовать панель инструментов + Duplicate Toolbar Дублировать панель инструментов + Delete Toolbar Удалить панель инструментов New {kind} Style… Новый стиль {kind}… New… Создать… Next Далее diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 4fdd6abf2..ab678406c 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2046,6 +2046,11 @@ font-technology Variable Variabla New Toolbar Nytt verktygsfält Show Toolbar Visa verktygsfält List Toolbars Lista verktygsfält + Manage Toolbars… Hantera verktygsfält… + Manage Toolbars Hantera verktygsfält + Rename Toolbar Byt namn på verktygsfält + Duplicate Toolbar Duplicera verktygsfält + Delete Toolbar Radera verktygsfält New {kind} Style… Ny {kind}stil… New… Ny… Next Nästa diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index ac0931554..78ffc5ae7 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -1918,6 +1918,11 @@ font-technology Variable Değişken New Toolbar Yeni Araç Çubuğu Show Toolbar Araç Çubuğunu Göster List Toolbars Araç Çubuklarını Listele + Manage Toolbars… Araç Çubuklarını Yönet… + Manage Toolbars Araç Çubuklarını Yönet + Rename Toolbar Araç Çubuğunu Yeniden Adlandır + Duplicate Toolbar Araç Çubuğunu Çoğalt + Delete Toolbar Araç Çubuğunu Sil New {kind} Style… Yeni {kind} Stili… New… Yeni… Next Sonraki diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index a7febfd3a..87a8a1a61 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2128,6 +2128,11 @@ file-filter Templates Шаблони New Toolbar Нова панель інструментів Show Toolbar Показати панель інструментів List Toolbars Список панелей інструментів + Manage Toolbars… Керування панелями інструментів… + Manage Toolbars Керування панелями інструментів + Rename Toolbar Перейменувати панель інструментів + Duplicate Toolbar Дублювати панель інструментів + Delete Toolbar Видалити панель інструментів New {kind} Style… Новий стиль {kind}… New… Створити… Next Далі diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 7c7d89b79..3856eef44 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2372,6 +2372,11 @@ digits Hindi Hindi New Toolbar Thanh công cụ mới Show Toolbar Hiện thanh công cụ List Toolbars Danh sách thanh công cụ + Manage Toolbars… Quản lý thanh công cụ… + Manage Toolbars Quản lý thanh công cụ + Rename Toolbar Đổi tên thanh công cụ + Duplicate Toolbar Nhân bản thanh công cụ + Delete Toolbar Xóa thanh công cụ New {kind} Style… Kiểu {kind} mới… New… Mới… Next Tiếp theo diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 2b70c03aa..78542f8a2 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2006,6 +2006,11 @@ file-filter Templates 模板 New Toolbar 新建工具栏 Show Toolbar 显示工具栏 List Toolbars 列出工具栏 + Manage Toolbars… 管理工具栏… + Manage Toolbars 管理工具栏 + Rename Toolbar 重命名工具栏 + Duplicate Toolbar 复制工具栏 + Delete Toolbar 删除工具栏 New {kind} Style… 新建{kind}样式… New… 新建… Next 下一个 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 26e592e8f..a7bcd23d8 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2983,6 +2983,11 @@ file-filter Templates 範本 New Toolbar 新增工具列 Show Toolbar 顯示工具列 List Toolbars 列出工具列 + Manage Toolbars… 管理工具列… + Manage Toolbars 管理工具列 + Rename Toolbar 重新命名工具列 + Duplicate Toolbar 製作工具列複本 + Delete Toolbar 刪除工具列 New {kind} Style… 新增{kind}樣式… New… 新增… Next 下一個 diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index d4677597a..46748f5db 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -497,6 +497,30 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{name: string, show?: bool} show (true), hide (false) or toggle (omitted) the toolbar made with New Toolbar called `name`, as its item under Window › Toolbars does (its title bar's × hides it); shown after Tab hid the toolbars, it shows alone and the others are turned off; returns the new state", ), + ( + "window.toolbar.manage", + "Manage Toolbars…", + "", + "{} open the Manage Toolbars dialog, which lists the toolbars made with New Toolbar: a name typed for the selected one renames it, + copies it (none selected: makes a blank toolbar) and the trash can deletes it", + ), + ( + "window.toolbar.rename", + "Rename Toolbar", + "", + "{name: string, to: string} give the toolbar made with New Toolbar called `name` the name `to` (not empty, at most 200 characters, not another toolbar's whatever the case), as typing a name in Manage Toolbars does; returns {toolbar: name}", + ), + ( + "window.toolbar.duplicate", + "Duplicate Toolbar", + "", + "{name?: string} copy the toolbar made with New Toolbar called `name`, with its tools, as \"`name` Copy\" (omitted: make a blank toolbar called \"Toolbar N\"), as Manage Toolbars' + does; the copy shows and is kept with the current workspace; returns {toolbar: name}", + ), + ( + "window.toolbar.delete", + "Delete Toolbar", + "", + "{name: string} delete the toolbar made with New Toolbar called `name`, and the groups torn off it, from the current workspace, as Manage Toolbars' trash can does", + ), ("window.toolbar.list", "List Toolbars", "", "{} the toolbars made with New Toolbar in the current workspace: [{name, shown, double, slots}]"), ("window.taskBar", "Contextual Task Bar", "", "{}"), ( diff --git a/docs/control-protocol.md b/docs/control-protocol.md index cdb7471df..b3555ec96 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -382,6 +382,16 @@ two-column flag, top-left corner, the groups torn off it). They are kept with th switching workspaces shows that workspace's toolbars, Reset Workspace keeps them, and New Workspace starts the new workspace with a copy of them. +Manage Toolbars: Window › Toolbars › Manage Toolbars… (or Manage Toolbars… in the drawer's ☰ menu, +`window.toolbar.manage`) lists these toolbars. A new name typed for the selected toolbar renames it at Enter, when +the text box loses focus, or at OK (`window.toolbar.rename {name, to}`; the name must not be empty, longer than 200 +characters or another toolbar's, whatever the case). The + button copies the selected toolbar with its tools as +"`name` Copy", shown a little below it and below earlier copies (`window.toolbar.duplicate {name}`), and with none +selected it makes a blank toolbar called "Toolbar N" (`window.toolbar.duplicate {}`). The trash can deletes the +selected toolbar and the groups torn off it (`window.toolbar.delete {name}`). Renaming or deleting a toolbar closes +the drawer and a group's flyout open on it; a new name that changes only the case leaves them open. Each change is +kept with the current workspace. + The Contextual Task Bar: its handle (the grip at its left end) drags it anywhere on the canvas, which always holds it whole, also when the window shrinks. Unpinned, the bar keeps that offset as it follows the selection, until another document becomes active. Its More Options (…) menu has Hide Bar (`window.taskBar`); Pin Bar Position diff --git a/docs/gaps.md b/docs/gaps.md index 3edba26df..77acb709a 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -601,7 +601,8 @@ from G14 and the budget run of G15: **~170–270 h** one agent, ~50–80 h wall - **Done:** View › Rulers › Change to Global / Artboard Rulers (`view.globalRulers`, Cmd+Alt+R): artboard rulers, a new document's, count from the active artboard, each artboard with its own zero point; rulers, X/Y fields and guide readouts follow the mode, which is saved with the document. Estimate unchanged. - **Done (#2849):** View › Rulers › Show Video Rulers (`view.videoRulers`): the rulers show and count the active artboard in video pixels from its top-left corner, a pixel as many points wide as the artboard's Video Ruler Pixel Aspect Ratio and one point tall ([public source](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/set-video-display-options-for-artboards.html)); Hide Video Rulers returns them to the General unit and the ruler origin. Shift-snapping a new guide to ruler ticks still uses the General unit's ticks. Estimate unchanged. - **Partial:** custom toolbars (#1759): Window › Toolbars › New Toolbar makes them, kept with the workspace they - were made in; Manage Toolbars (rename, copy, delete) is missing. Inside the estimate below. + were made in, and Window › Toolbars › Manage Toolbars renames, copies and deletes them; docking them is missing. + Inside the estimate below. - **Done (#1753):** a saved workspace keeps the width of the dock column. - **Done (#4135):** floating panels open in OS windows of their own on the desktop, so they can sit on another monitor; on the web and on Wayland they float inside the window. - **Done:** View › New View… and Edit Views… ([public source](https://helpx.adobe.com/illustrator/using/viewing-artwork.html)): a document keeps up to 25 named views (zoom, centre and rotation), saved with the file; every one is listed by name at the bottom of the View menu (`view.goto1`–`view.goto25`), and choosing one changes the current window to it. Edit Views… lists them to rename or delete, applied together on OK as one undo step (`view.saved.edit {edits}`); a file's views are checked when it opens. Estimate unchanged. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6bb9818b0..1ad03aa3d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -618,7 +618,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Screen readers get the value of the color and channel sliders that panels and dialogs draw (the Color panel, the Color Picker, Swatch Options, the Navigator's Zoom, the Character panel's variable font axes, the Perspective Grid and flattener options, and the sliders of the effect, tool option and transform dialogs): the number and text of the field beside each one, such as 128 for G or 50% for an opacity, with the slider's range, which extends to include a number past the slider's end. The harmony wheel's Brightness slider, which has no field, reports its brightness in percent. - Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel (in an OS window of its own on the desktop, #4135), and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on a dock group's tabs, which light up under the pointer, its panels join that group, and on the group's upper or lower half they make a new group above or below it (`window.panel.dock {anchor, zone}`); dropped on the icon column they become icons there, the tabbed group's panels too (`window.panel.dock {icon: true}`); dropped anywhere else on the dock, which lights up, they go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`), and a dock group too, a group below it moving up into the room; a second double-click, or a click on another of its tabs, expands it (`window.panel.collapse`, kept with the preferences and in saved workspaces). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set. - The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done. - - Window › Toolbars › New Toolbar… and New Toolbar… in the All Tools drawer's menu ask for a name and make a blank toolbar that floats on its own, moved by its title bar and listed under Window › Toolbars, whose item shows or hides it. The title bar's × hides it, and its double arrow shows its tools in one column or two. Its ••• button opens the All Tools drawer beside it, and tools dragged from the drawer fill it. The toolbars are kept with the workspace they were made in: switching workspaces shows that workspace's toolbars, Reset Workspace keeps them, and New Workspace starts the new workspace with a copy of them (; `window.toolbar.new`, `window.toolbar.custom`, the `toolbar` param of `window.editToolbar` and `toolbar.addTool`/`removeTool`/`reset`; `tools_added_to_a_new_toolbar_survive_a_workspace_reload`). Manage Toolbars is not done. + - Window › Toolbars › New Toolbar… and New Toolbar… in the All Tools drawer's menu ask for a name and make a blank toolbar that floats on its own, moved by its title bar and listed under Window › Toolbars, whose item shows or hides it. The title bar's × hides it, and its double arrow shows its tools in one column or two. Its ••• button opens the All Tools drawer beside it, and tools dragged from the drawer fill it. The toolbars are kept with the workspace they were made in: switching workspaces shows that workspace's toolbars, Reset Workspace keeps them, and New Workspace starts the new workspace with a copy of them (; `window.toolbar.new`, `window.toolbar.custom`, the `toolbar` param of `window.editToolbar` and `toolbar.addTool`/`removeTool`/`reset`; `tools_added_to_a_new_toolbar_survive_a_workspace_reload`). + - Window › Toolbars › Manage Toolbars… and Manage Toolbars… in the All Tools drawer's menu open a dialog that lists the toolbars made with New Toolbar: a new name typed for the selected toolbar renames it, + copies it with its tools (with none selected, + makes a blank toolbar), and the trash can deletes it. Each change shows under Window › Toolbars and stays in the workspace (; `window.toolbar.manage`, `window.toolbar.rename`, `window.toolbar.duplicate`, `window.toolbar.delete`; `manage_toolbars_renames_duplicates_and_deletes`). - Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels. Every complete catalog has rows for these labels and for the Roman and Japanese printer mark styles. Evidence: `i18n::tests::czech_render_site_follow_up`, `i18n::tests::complete_catalogs_translate_render_site_labels`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md). - The History panel, Edit › Undo and Edit › Redo translate the names of the steps that apply or redefine a character or paragraph style and of the step that redefines a graphic style: each such step keeps its template (`Apply {name}`, `Redefine {name}`, `Redefine Graphic Style “{name}”`) and the style's name, every complete catalog has a row for each template, and the panel and the menu show the style's name as the document holds it, except the Normal styles' names, which they show in the interface language (`steps_named_after_a_style_list_the_template_around_the_name`, `undo_and_redo_name_a_style_step_around_the_style_name`). - Localised UI: menus (in-window and native), panels, dialogs, toolbar and Preferences read from per-language catalogs (`crates/ui-egui/src/i18n`, see `docs/development.md`); Traditional Chinese (Taiwan), Simplified Chinese (mainland), Japanese, Korean, Vietnamese, German, Spanish, French, Italian, Polish, Brazilian Portuguese, Russian, Ukrainian, Georgian and Catalan ship complete, and Czech covers every menu label (and more), with system-locale detection on macOS, Windows, Linux and the web (the browser's languages, or `?lang=`), a Language preference and, on macOS, a VectorCraft ▸ Language menu. Names are never translated (fonts and their styles, layers, artboards, saved presets, user libraries, custom workspaces, recent files, plug-ins). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 77a9ca64a..5499b09aa 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -309,8 +309,9 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po | Panels dragged onto a dock group's tabs join it; onto its upper or lower half they make a new group above or below it (a top/bottom split, resizable); onto the icon column they become icons, the tabbed group's panels too; a dock tab's context menu closes it (Window menu reopens it) | done | `window.panel.dock {anchor, zone}`, `window.panel.dock {icon: true}`, `window.panel.close` | | A double-click on a dock group's tab collapses the group to its tabs (a group below it moves up into the room) and a second one expands it; a click on another of its tabs expands it showing that panel ([helpx: Workspace basics](https://helpx.adobe.com/illustrator/using/workspace-basics.html), panel groups) | done | `window.panel.collapse {panel, collapsed?}`; in dock groups and floating windows; a group beside another (a left/right split) doesn't collapse | | Panels outside the app window (another monitor) | done | floating groups in OS windows of their own on the desktop (#4135) | -| Edit Toolbar (•••): the All Tools drawer opens beside the toolbar by its button (Escape or a click outside closes it), dims the tools already in the toolbar; while it is open, tools drag from it onto the toolbar (between two tools, or onto one to join its group), from the toolbar into it, and within the toolbar; each toolbar keeps its edits | done | #1018; `window.editToolbar`, `toolbar.addTool`/`removeTool`/`reset`; the drawer's Reset (Illustrator has it in a drawer menu); dragging a toolbar button moves the tool it shows, not its whole group; Manage Toolbars missing; tool flyouts are as wide as their longest translated name and shortcut need (#1616) | +| Edit Toolbar (•••): the All Tools drawer opens beside the toolbar by its button (Escape or a click outside closes it), dims the tools already in the toolbar; while it is open, tools drag from it onto the toolbar (between two tools, or onto one to join its group), from the toolbar into it, and within the toolbar; each toolbar keeps its edits | done | #1018; `window.editToolbar`, `toolbar.addTool`/`removeTool`/`reset`; the drawer's Reset (Illustrator has it in a drawer menu); dragging a toolbar button moves the tool it shows, not its whole group; tool flyouts are as wide as their longest translated name and shortcut need (#1616) | | Window › Toolbars › New Toolbar… (also in the All Tools drawer's menu): a name and OK make a blank floating toolbar, listed under Window › Toolbars (its item shows or hides it); its title bar moves it, its × hides it and its double arrow shows its tools in one column or two; its ••• button opens the All Tools drawer beside it to fill it; kept with the workspace it was made in | done | #1759 in part; `window.toolbar.new`, `window.toolbar.custom`, `window.toolbar.list`, the `toolbar` param of `window.toolbarColumns`, `window.editToolbar` and `toolbar.addTool`/`removeTool`/`reset` | +| Window › Toolbars › Manage Toolbars… (also in the All Tools drawer's menu): the dialog lists the toolbars made with New Toolbar; a new name typed for the selected one renames it, + copies it (none selected: a blank toolbar) and the trash can deletes it; the changes stay in the workspace | done | #1759 in part; `window.toolbar.manage`, `window.toolbar.rename`, `window.toolbar.duplicate`, `window.toolbar.delete` | | Quick Image Trace presets trace at once and leave the panel as it was; the Image Trace button (Control bar, Properties) opens the editable controls | done | Per the public documentation, choosing a preset traces the image with it straight away, and the panel is opened with the Image Trace button in the Control panel or in Properties' Image Trace section (). The controls show Threshold, Paths, Corners and Noise with Advanced open; `panels/image_trace.rs` headless tests (`a_quick_trace_leaves_the_panel_as_it_was`, `sketch_quick_trace_reveals_all_adjustments_and_keeps_the_source` for the button's route). No algorithm parity claim. | | Image Trace panel: holding the eye button next to View down shows the selected Image Trace objects' source image on the canvas | done | Per the public documentation, the control next to View is pressed and held to compare the trace result with the source image (). On release the objects show their View again; the document, its View and the undo history are unchanged. `RenderOptions::trace_source`; `panels/image_trace.rs` headless test `holding_the_eye_button_shows_the_source_image_until_released`, render test `trace_source_draws_the_source_image_of_the_listed_objects`. The Control bar and Properties have no such button | | Live trace result bounds and selection highlights exclude the retained bitmap; source-image views retain it | done | All TraceView modes and rotated result bounds regression-covered; ordinary hidden group behavior retained |