Toolbars: Manage Toolbars renames, copies and deletes the toolbars made with New Toolbar

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 "<name> Copy" (then "<name> 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
This commit is contained in:
dotCooCoo
2026-11-04 10:26:01 +07:00
committed by bflatastic
parent a0a4633c26
commit de78142dd8
34 changed files with 696 additions and 11 deletions
+514 -3
View File
@@ -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:
//! <https://helpx.adobe.com/illustrator/using/tools.html> (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<usize, String> {
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::<Option<String>>(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<String, String> {
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<String, String> {
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<R
}
Ok(json!(shown))
})(),
"window.toolbar.manage" => {
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<bool, String> {
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<Value, String> {
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<String> = None;
let mut action: Option<bool> = 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<String>, mut action: Option<bool>) {
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<crate::menus::Item> {
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<crate::menus::Item> {
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::<Vec<_>>(), ["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]
+1
View File
@@ -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,
+11 -5
View File
@@ -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
+5
View File
@@ -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 Следващ
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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 Δεν υπάρχει έγγραφο
Can't render this file because it contains an unexpected character in line 163 and column 5.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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ő
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
603
604
605
606
607
608
609
610
611
612
613
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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 コンテキストタスクバー
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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 კონტექსტური ამოცანების ზოლი
Can't render this file because it is too large.
+5
View File
@@ -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 상황별 작업 표시줄
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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ń
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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…
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -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 Далее
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+5
View File
@@ -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 Далі
Can't render this file because it contains an unexpected character in line 177 and column 5.
+5
View File
@@ -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
Can't render this file because it contains an unexpected character in line 199 and column 5.
+5
View File
@@ -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 下一个
Can't render this file because it contains an unexpected character in line 2 and column 73.
+5
View File
@@ -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 下一個
Can't render this file because it contains an unexpected character in line 6 and column 47.
+24
View File
@@ -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", "", "{}"),
(
+10
View File
@@ -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
+2 -1
View File
@@ -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.
+2 -1
View File
@@ -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 (<https://helpx.adobe.com/illustrator/using/tools.html>; `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 (<https://helpx.adobe.com/illustrator/using/tools.html>; `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 (<https://helpx.adobe.com/illustrator/using/tools.html>; `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).
+2 -1
View File
@@ -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 (<https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html>). 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 (<https://web.archive.org/web/20250831090623/https://helpx.adobe.com/illustrator/using/image-trace.html>). 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 |