From c847acdf238ac24bcb0658e787badb0bfd48e3d7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 01:29:58 -0800 Subject: [PATCH 1/2] Palette: a pick opens the dialog its menu item opens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pick in Search Commands ran its command with no params, while the menu bar item gives the command params and opens a dialog for them. Clean Up, Create Gradient Mesh, Smart Punctuation, Release Type on a Path and Deep Ungroup ran at once with their default values, Find and Replace reported "missing `find`", and Print Ink Fixes reported that it needs a value. A pick now runs what a click on the menu item runs, so these open their dialogs and nothing changes until the dialog is confirmed. While the menu item is dimmed (Remove Background or Object Mosaic with no image selected, Find and Replace with no document open), a pick and a shortcut open no dialog, and the status line says why the command can't run. Before this, a shortcut of such a command opened its dialog, and confirming it failed. Both routes use one helper, shortcut_editor::menu_target_now. The menu items that helper reads are read once. Items that list an installed plug-in (File › Open With importers, Object › Plug-ins filters) are now left out of them, so a plug-in installed or removed later does not leave an item there that the menu no longer shows. --- crates/ui-egui/src/menus.rs | 1 - crates/ui-egui/src/palette.rs | 166 ++++++++++++++++++++++++-- crates/ui-egui/src/shortcut_editor.rs | 55 +++++++-- crates/ui-egui/src/shortcuts.rs | 14 ++- crates/ui-egui/src/shortcuts/tests.rs | 45 +++++++ 5 files changed, 257 insertions(+), 24 deletions(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 1698e06ed4..5651a6ff34 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -71,7 +71,6 @@ pub(crate) use invoke::graph_dialog; #[cfg(test)] pub(crate) use invoke::with_held_keys; pub use invoke::{click_target, invoke, invoke_from_system_menu}; -#[cfg(test)] pub(crate) use labels::lists_plugin; #[cfg(test)] use labels::shows_a_name; diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index d5bf7b6098..1a58c496b2 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -91,7 +91,10 @@ fn build(lang: crate::i18n::Lang) -> Vec { } /// Runs the palette result `id`: selects a tool, or invokes a command with the params its entry -/// stands for. +/// stands for. A command whose menu bar item gives params runs what a click on that item runs +/// ([`crate::shortcut_editor::menu_target_now`]), so Clean Up… and Find and Replace… open their +/// dialogs. While that item is dimmed, no dialog opens and the status line says why the command +/// can't run. pub(crate) fn run_pick(app: &mut VectorcraftApp, id: &str) { if let Some(tool) = id.strip_prefix("tool:") { app.select_tool(tool); @@ -99,7 +102,11 @@ pub(crate) fn run_pick(app: &mut VectorcraftApp, id: &str) { let command = crate::keymap::command_of(id); menus::invoke(app, &command.id, command.params); } else { - menus::invoke(app, id, json!({})); + match crate::shortcut_editor::menu_target_now(app, id) { + Some(Ok((id, p))) => menus::invoke(app, &id, p), + Some(Err(why)) => app.status(why), + None => menus::invoke(app, id, json!({})), + } } } @@ -217,7 +224,7 @@ mod tests { let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); let before = app.session.prefs.use_precise_cursors; let first = matches.first().expect("query has a result"); - menus::invoke(&mut app, &first.id, json!({})); + run_pick(&mut app, &first.id); assert_ne!(app.session.prefs.use_precise_cursors, before, "first result toggles the preference"); } @@ -238,8 +245,7 @@ mod tests { app.run("select.set", json!({"ids": [a, b]})).unwrap(); app.run("select.key", json!({"id": b})).unwrap(); assert!(app.session.active().unwrap().selection.key.is_some()); - // Run a pick the way `show` runs it. - menus::invoke(&mut app, ids[0], json!({})); + run_pick(&mut app, ids[0]); let st = app.session.active().unwrap(); assert_eq!((st.selection.len(), st.selection.key), (2, None), "both objects stay selected, without a key"); } @@ -255,20 +261,162 @@ mod tests { app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); app.ui.status.clear(); assert!(!crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}"); - // Run a pick the way `show` runs it. - menus::invoke(&mut app, id, json!({})); + run_pick(&mut app, id); assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}: {}", app.ui.status); assert!(!app.ui.status.contains("invalid parameters"), "{id}: {}", app.ui.status); // A second pick leaves the open panel open, docked or floating. - menus::invoke(&mut app, id, json!({})); + run_pick(&mut app, id); assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}: picked twice"); app.run("window.panel.float", json!({"panel": crate::panels::asset_export::ID})).unwrap(); assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}: floating"); - menus::invoke(&mut app, id, json!({})); + run_pick(&mut app, id); assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}: floating, picked again"); } } + /// The open dialog's kind and fields, the active document's undo depth and the status line. + type Outcome = (Option<(String, serde_json::Map)>, Option, String); + + /// The [`Outcome`] `app` shows. + fn outcome(app: &VectorcraftApp) -> Outcome { + ( + app.ui.dialog.as_ref().map(|d| (d.kind.clone(), d.fields.clone())), + app.session.active().map(|st| st.history.undo.len()), + app.ui.status.clone(), + ) + } + + /// A pick of a "…" command whose menu bar item gives params opens that item's dialog, and Smooth + /// opens its slider; nothing changes until the dialog is confirmed. + #[test] + fn a_pick_opens_the_dialog_its_menu_item_opens() { + let expected = [ + ("object.path.cleanUp", "command"), + ("object.mesh.create", "command"), + ("type.smartPunctuation", "command"), + ("edit.findReplace", "command"), + ("object.path.smooth", crate::dialogs::smooth_slider::KIND), + ]; + for (id, kind) in expected { + assert!(items().iter().any(|(_, item, _)| item == id), "{id} is in the palette"); + let mut app = VectorcraftApp::new(vectorcraft_testkit::fixtures::Fixture::Single.session(), crate::Services::default()); + app.ui.status.clear(); + let before = app.session.active().unwrap().history.undo.len(); + run_pick(&mut app, id); + let d = app.ui.dialog.as_ref().unwrap_or_else(|| panic!("{id} opens its dialog: {}", app.ui.status)); + assert_eq!(d.kind, kind, "{id}"); + if kind == "command" { + assert_eq!(d.fields["__command"], json!(id), "{id}: {:?}", d.fields); + } + assert_eq!(app.session.active().unwrap().history.undo.len(), before, "{id} changes nothing yet"); + assert!(app.ui.status.is_empty(), "{id}: {}", app.ui.status); + } + } + + /// Every palette command whose menu bar items all give it the same params does what a click on + /// such an item does, as the menu row shows it ([`menus::entry`]): with no document open, in an + /// empty document and with one or several objects selected, the pick opens the same dialog with + /// the same fields and leaves the same undo steps and status. While the row is dimmed, the pick + /// opens no dialog, changes nothing and says why the command can't run. + #[test] + fn every_pick_runs_what_its_menu_item_runs() { + use vectorcraft_testkit::fixtures::Fixture; + fn walk<'a>(items: &'a [menus::Item], out: &mut Vec<&'a menus::Item>) { + for it in items { + match it { + menus::Item::Sub(_, children) => walk(children, out), + // Items that list an installed plug-in come and go as tests beside this one + // install plug-ins, and no shortcut or pick reads them. + menus::Item::Cmd(_, id, p) if !menus::lists_plugin(id, p) => out.push(it), + _ => {} + } + } + } + let tree = menus::menu_tree(); + let mut rows = vec![]; + for (_, items) in &tree { + walk(items, &mut rows); + } + let mut checked: Vec<(String, &menus::Item)> = vec![]; + for (_, id, _) in items() { + let of_id: Vec<&menus::Item> = rows.iter().copied().filter(|it| matches!(it, menus::Item::Cmd(_, c, _) if *c == id)).collect(); + let params: Vec<&serde_json::Value> = + of_id.iter().filter_map(|it| if let menus::Item::Cmd(_, _, p) = it { Some(p) } else { None }).collect(); + let Some(first) = params.first() else { continue }; + if first.as_object().is_none_or(serde_json::Map::is_empty) || params.iter().any(|p| p != first) { + continue; + } + checked.push((id, of_id[0])); + } + assert!(checked.iter().any(|(id, _)| id == "edit.findReplace"), "{:?}", checked.iter().map(|c| &c.0).collect::>()); + let state = |n: usize| match n.checked_sub(1) { + None => vectorcraft_engine::Session::new(), + Some(k) => Fixture::ALL[k].session(), + }; + let (mut compared, mut dimmed) = (0, 0); + for (id, item) in &checked { + for n in 0..=Fixture::ALL.len() { + let mut picked = VectorcraftApp::new(state(n), crate::Services::default()); + let mut clicked = VectorcraftApp::new(state(n), crate::Services::default()); + picked.ui.status.clear(); + clicked.ui.status.clear(); + // A row the menu leaves out in this state has no click to compare with. + let Some(menus::Entry::Item(row)) = menus::entry(&clicked, item) else { continue }; + let (target, enabled) = (row.target().expect("the item is implemented"), row.enabled); + let before = outcome(&picked); + run_pick(&mut picked, id); + if enabled { + menus::invoke(&mut clicked, &target.0, target.1); + assert_eq!(outcome(&picked), outcome(&clicked), "{id} [state {n}]"); + compared += 1; + } else { + let after = outcome(&picked); + assert_eq!((after.0, after.1), (None, before.1), "{id} [state {n}] is dimmed"); + assert!(after.2.contains("not available"), "{id} [state {n}]: {}", after.2); + dimmed += 1; + } + } + } + assert!(compared > 0 && dimmed > 0, "{compared} compared, {dimmed} dimmed"); + } + + /// A pick of a "…" command whose menu bar item is dimmed (Remove Background and Object Mosaic + /// with no image selected, Find and Replace with no document open) opens no dialog, changes + /// nothing and reports why the command can't run. + #[test] + fn a_pick_of_a_dimmed_command_says_why_it_cannot_run() { + let cases = [ + ("addon.image.removeBackground", vectorcraft_testkit::fixtures::Fixture::Multi.session()), + ("object.createObjectMosaic", vectorcraft_testkit::fixtures::Fixture::Multi.session()), + ("edit.findReplace", vectorcraft_engine::Session::new()), + ]; + for (id, session) in cases { + assert!(crate::shortcut_editor::menu_target(id).is_some(), "{id} has a dialog"); + let mut app = VectorcraftApp::new(session, crate::Services::default()); + assert!(!menus::enabled(&app, id), "{id}"); + app.ui.status.clear(); + let before = app.session.active().map(|st| st.history.undo.len()); + run_pick(&mut app, id); + assert!(app.ui.dialog.is_none(), "{id}"); + assert_eq!(app.session.active().map(|st| st.history.undo.len()), before, "{id}"); + assert!(app.ui.status.contains("not available"), "{id}: {}", app.ui.status); + } + } + + /// A pick of a command that opens no dialog runs it at once. + #[test] + fn a_pick_of_a_command_without_a_dialog_runs_at_once() { + for id in ["object.group", "paint.swap"] { + assert!(items().iter().any(|(_, item, _)| item == id), "{id} is in the palette"); + assert_eq!(crate::shortcut_editor::menu_target(id), None, "{id}"); + let mut app = VectorcraftApp::new(vectorcraft_testkit::fixtures::Fixture::Multi.session(), crate::Services::default()); + let before = app.session.active().unwrap().history.undo.len(); + run_pick(&mut app, id); + assert!(app.ui.dialog.is_none(), "{id}"); + assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1, "{id}: {}", app.ui.status); + } + } + /// #1757: Up and Down move the highlighted result, wrapping at the ends; a new query starts /// from the first again. #[test] diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index 7ada99ee5c..a580b2255a 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -653,23 +653,28 @@ const NEEDS_A_PARAM: [&str; 20] = [ /// The (label, params) of each menu bar item, by command id. fn menu_bar_items() -> &'static HashMap<&'static str, Vec<(&'static str, Value)>> { + static ITEMS: OnceLock>> = OnceLock::new(); + ITEMS.get_or_init(|| bar_items(&menus::menu_tree())) +} + +/// The (label, params) of each item of the menus `tree`, by command id. Items that list an +/// installed plug-in ([`menus::lists_plugin`]) are left out: [`menu_bar_items`] is built once, +/// and a plug-in installed or removed later would leave it with a row the menu no longer shows. +fn bar_items(tree: &[(&'static str, Vec)]) -> HashMap<&'static str, Vec<(&'static str, Value)>> { fn walk(items: &[menus::Item], out: &mut HashMap<&'static str, Vec<(&'static str, Value)>>) { for it in items { match it { - menus::Item::Cmd(label, id, p) => out.entry(*id).or_default().push((*label, p.clone())), + menus::Item::Cmd(label, id, p) if !menus::lists_plugin(id, p) => out.entry(*id).or_default().push((*label, p.clone())), menus::Item::Sub(_, children) => walk(children, out), _ => {} } } } - static ITEMS: OnceLock>> = OnceLock::new(); - ITEMS.get_or_init(|| { - let mut out = HashMap::new(); - for (_, items) in menus::menu_tree() { - walk(&items, &mut out); - } - out - }) + let mut out = HashMap::new(); + for (_, items) in tree { + walk(items, &mut out); + } + out } fn is_bare(p: &Value) -> bool { @@ -686,6 +691,19 @@ pub(crate) fn menu_target(id: &str) -> Option<(String, Value)> { items.iter().all(|(_, q)| q == p).then(|| menus::click_target(label, id, p)) } +/// What a command search pick, or a shortcut without params, of command `id` runs now: `Ok` with +/// what a click on its menu bar item runs ([`menu_target`]), or `None` when no such item gives it +/// params. While that item is dimmed, no dialog opens for it: `Err` holds the status line that says +/// why the command can't run (Remove Background… with no image selected). +pub(crate) fn menu_target_now(app: &VectorcraftApp, id: &str) -> Option> { + let target = menu_target(id)?; + if menus::enabled(app, id) { + return Some(Ok(target)); + } + let why = (vectorcraft_engine::find_command(id)?.enabled)(&app.session).err()?; + Some(Err(vectorcraft_engine::EngineError::Disabled(id.to_string(), why).to_string())) +} + /// Is `key` a command with no default shortcut that a key press can't run: no item of the menu /// bar runs it without params or with one set of params ([`menu_target`]), it opens no dialog when /// run without them (as the Variables panel's commands do), and it is a query, whose answer a key @@ -2270,6 +2288,25 @@ mod tests { assert_eq!(menu_target("edit.undo"), None, "runs without params"); } + /// The menu bar items a shortcut or a command search pick reads leave out items that list an + /// installed plug-in (a File › Open With importer, an Object › Plug-ins filter), which come and + /// go after the items are read; the other items stay. + #[test] + fn menu_bar_items_leave_out_installed_plug_ins() { + let tree = vec![( + "File", + vec![ + menus::Item::Sub("Open With", vec![menus::Item::Cmd("Spins…", "file.openWith", json!({"importer": "plugin.test.spins"}))]), + menus::Item::Cmd("Filter…", "plugin.dialog", json!({"id": "test.filter"})), + menus::Item::Cmd("Clean Up…", "object.path.cleanUp", json!({"strayPoints": true})), + ], + )]; + let items = bar_items(&tree); + assert!(!items.contains_key("file.openWith"), "{items:?}"); + assert!(!items.contains_key("plugin.dialog"), "{items:?}"); + assert_eq!(items.get("object.path.cleanUp"), Some(&vec![("Clean Up…", json!({"strayPoints": true}))])); + } + /// Every command the editor lists, run as a key press runs it on a document with one or with /// several objects selected, reports no missing param. #[test] diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 83d53d700c..f0ff34ef64 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -985,9 +985,10 @@ fn dispatch(app: &mut VectorcraftApp, ctx: &egui::Context, accept: impl FnMut(&H } /// What a shortcut without params runs when its command's menu bar item gives params -/// ([`crate::shortcut_editor::menu_target`]): what a click on that item runs. -fn menu_target(c: &CommandRef) -> Option<(String, serde_json::Value)> { - c.params.as_object().is_none_or(serde_json::Map::is_empty).then(|| crate::shortcut_editor::menu_target(&c.id)).flatten() +/// ([`crate::shortcut_editor::menu_target_now`]): what a click on that item runs, or while the item +/// is dimmed, why the command can't run. +fn menu_target(app: &VectorcraftApp, c: &CommandRef) -> Option> { + c.params.as_object().is_none_or(serde_json::Map::is_empty).then(|| crate::shortcut_editor::menu_target_now(app, &c.id)).flatten() } /// Params used when a shortcut invokes its command. The named panel function keys retain their @@ -1030,8 +1031,11 @@ fn run_hit(app: &mut VectorcraftApp, ctx: &egui::Context, c: &CommandRef) { // every artboard instead of the art. crate::panels::artboards::select_all(app); } else { - let (id, p) = menu_target(c).unwrap_or_else(|| (c.id.clone(), shortcut_params(c))); - crate::menus::invoke(app, &id, p); + match menu_target(app, c) { + Some(Ok((id, p))) => crate::menus::invoke(app, &id, p), + Some(Err(why)) => app.status(why), + None => crate::menus::invoke(app, &c.id, shortcut_params(c)), + } } } diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index 8d32a4de22..bdc6efbc52 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -412,6 +412,28 @@ fn a_single_key_shortcut_without_a_dialog_runs_at_once() { assert_eq!((open_dialog(&app), undo_len(&app)), (None, before + 1), "Shift+X swaps fill and stroke: {}", app.ui.status); } +/// A key or a chord of a command whose menu item is dimmed opens no dialog: Remove Background… and +/// Object Mosaic… with a rectangle selected change nothing and say why they can't run. +#[test] +fn a_shortcut_of_a_dimmed_command_opens_no_dialog_and_says_why() { + let chord = vec![ + egui::Event::ModifiersChanged(cmd() | Modifiers::ALT | Modifiers::SHIFT), + egui::Event::Key { key: Key::F9, physical_key: None, pressed: true, repeat: false, modifiers: cmd() | Modifiers::ALT | Modifiers::SHIFT }, + ]; + for id in ["addon.image.removeBackground", "object.createObjectMosaic"] { + assert!(crate::shortcut_editor::menu_target(id).is_some(), "{id} has a dialog"); + for (shortcut, events) in [("F", press_f()), ("Cmd+Alt+Shift+F9", chord.clone())] { + let mut app = app_with_shortcut(id, shortcut); + assert!(!crate::menus::enabled(&app, id), "{id}"); + let before = undo_len(&app); + frame(&mut app, events); + crate::shortcut_editor::sync(&crate::state::UiState::default()); + assert_eq!((open_dialog(&app), undo_len(&app)), (None, before), "{id} on {shortcut}: {}", app.ui.status); + assert!(app.ui.status.contains("not available"), "{id} on {shortcut}: {}", app.ui.status); + } + } +} + /// With no document open, a command's key without a modifier reports what the same command /// reports on a chord. #[test] @@ -1616,6 +1638,29 @@ fn the_command_search_that_opens_while_enter_is_down_waits_for_a_new_press() { assert!(!app.ui.palette_open, "a new press runs the highlighted result"); } +/// Enter on a command search result whose menu item opens a dialog (Create Gradient Mesh…) opens +/// that dialog: the press and its repeats do not confirm it, and a new press does. +#[test] +fn enter_on_a_command_search_result_opens_its_dialog_and_a_new_press_confirms_it() { + let mut app = app_with_a_selected_rectangle(); + let held = HeldKey::new(Key::Enter, Modifiers::NONE); + app.select_tool("selection"); + app.ui.palette_open = true; + app.ui.palette_query = "object.mesh.create".into(); + let before = undo_len(&app); + for repeat in 0..4 { + held.press(&mut app); + let command = app.ui.dialog.as_ref().and_then(|d| d.fields.get("__command").cloned()); + assert_eq!(command, Some(json!("object.mesh.create")), "repeat {repeat}: its dialog stays open: {}", app.ui.status); + assert_eq!(undo_len(&app), before, "repeat {repeat}: nothing changed"); + } + assert!(!app.ui.palette_open); + held.release(&mut app); + held.press(&mut app); + assert_eq!(open_dialog(&app), None, "a new press confirms"); + assert_eq!(undo_len(&app), before + 1, "the mesh is one undo step: {}", app.ui.status); +} + /// A field that rejects its text on Enter keeps the dialog open, while the key is held too. #[test] fn holding_enter_on_a_rejected_value_leaves_the_dialog_open() { From c1fee5d7a3607619cfa27614afd890e0e5bb264f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 06:10:30 -0800 Subject: [PATCH 2/2] Palette: picks and shortcuts skip menu rows that name user data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The menu items that a pick or a shortcut reads are read once. Rows that show a name that is user data (a custom toolbar under Window › Toolbars, a script under File › Scripts, a recent file, a saved selection) are now left out of them, as rows that list a plug-in already are, so a pick of Show Toolbar no longer runs with the name of a toolbar that existed when the items were read. While a menu item is dimmed and the engine gives no reason (a UI command, or an Appearance command with nothing targeted), a pick or a shortcut now opens no dialog and shows "command `` is not available right now". The message has a row in every catalog that has the engine's longer "not available right now" message. --- crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/bs.tsv | 1 + crates/ui-egui/src/i18n/ca.tsv | 1 + crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/da.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fi.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hi.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ka.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/sv.tsv | 1 + crates/ui-egui/src/i18n/tr.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/menus.rs | 3 +- crates/ui-egui/src/menus/labels.rs | 2 +- crates/ui-egui/src/palette.rs | 7 ++-- crates/ui-egui/src/shortcut_editor.rs | 57 +++++++++++++++++++++------ crates/ui-egui/src/shortcuts/tests.rs | 20 +++++++--- 32 files changed, 92 insertions(+), 24 deletions(-) diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 97e5b335d3..adad8f49ff 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -4239,6 +4239,7 @@ axis Both И двете @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` не е цвят като "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: най-много {MAX_TABLE} цвята @msg command `{0}` is not available right now: {1} команда `{0}` не е налична в момента: {1} +@msg command `{id}` is not available right now команда `{id}` не е налична в момента @msg compound paths can only contain paths съставните пътища могат да съдържат само пътища @msg compressed (RLE) bitmaps can't be read yet компресираните (RLE) битови карти все още не могат да бъдат четени @msg damaged повреден diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 75353321ff..bc01e380ad 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -4627,6 +4627,7 @@ imageTrace Outline Obris @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` nije boja poput "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: najviše {MAX_TABLE} boja @msg command `{0}` is not available right now: {1} naredba `{0}` trenutno nije dostupna: {1} +@msg command `{id}` is not available right now naredba `{id}` trenutno nije dostupna @msg compound paths can only contain paths složene putanje mogu sadržavati samo putanje @msg compressed (RLE) bitmaps can't be read yet komprimirane (RLE) bitmape se još ne mogu čitati @msg damaged oštećeno diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 75e0b9ea20..1ebf8f304e 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -5280,6 +5280,7 @@ digits Malayalam Malaiàlam @msg column must be true or a number from 1 column ha de ser true o un nombre a partir d’1 @msg Combine Slices: the slices have no bounds Combina els talls: els talls no tenen límits @msg command `{0}` is not available right now: {1} l’ordre `{0}` no està disponible ara mateix: {1} +@msg command `{id}` is not available right now l’ordre `{id}` no està disponible ara mateix @msg compound paths can only contain paths els traçats compostos només poden contenir traçats @msg compressed (RLE) bitmaps can't be read yet encara no es poden llegir els mapes de bits comprimits (RLE) @msg control must be fillStroke, coloring, drawMode or screenMode control ha de ser fillStroke, coloring, drawMode o screenMode diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 5d37ee136e..b4677f2f30 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -4704,6 +4704,7 @@ imageTrace Outline Obrys @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` není barva jako "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: nejvýše {MAX_TABLE} barev @msg command `{0}` is not available right now: {1} příkaz `{0}` nyní není k dispozici: {1} +@msg command `{id}` is not available right now příkaz `{id}` nyní není k dispozici @msg compound paths can only contain paths složené cesty mohou obsahovat jen cesty @msg compressed (RLE) bitmaps can't be read yet komprimované bitmapy (RLE) zatím nelze načíst @msg damaged poškozeno diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 8167f80138..bbb1d8e2dc 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -4351,6 +4351,7 @@ imageTrace Outline Kontur @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` er ikke en farve som "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: højst {MAX_TABLE} farver @msg command `{0}` is not available right now: {1} kommandoen `{0}` er ikke tilgængelig lige nu: {1} +@msg command `{id}` is not available right now kommandoen `{id}` er ikke tilgængelig lige nu @msg compound paths can only contain paths sammensatte kurver kan kun indeholde kurver @msg compressed (RLE) bitmaps can't be read yet komprimerede (RLE) bitmaps kan endnu ikke læses @msg damaged beskadiget diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index bd4bb38b43..c478fbe089 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -4319,6 +4319,7 @@ theme Appearance Erscheinungsbild @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` ist keine Farbe wie "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: höchstens {MAX_TABLE} Farben @msg command `{0}` is not available right now: {1} der Befehl `{0}` ist derzeit nicht verfügbar: {1} +@msg command `{id}` is not available right now der Befehl `{id}` ist derzeit nicht verfügbar @msg compound paths can only contain paths zusammengesetzte Pfade können nur Pfade enthalten @msg compressed (RLE) bitmaps can't be read yet komprimierte Bitmaps (RLE) können noch nicht gelesen werden @msg damaged beschädigt diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 9860f8ad50..15e4a1d35e 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -4350,6 +4350,7 @@ digits Malayalam Μαλαγιάλαμ @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: το `{h}` δεν είναι έγκυρο χρώμα, όπως το "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: επιτρέπονται το πολύ {MAX_TABLE} χρώματα @msg command `{0}` is not available right now: {1} η εντολή `{0}` δεν είναι διαθέσιμη αυτήν τη στιγμή: {1} +@msg command `{id}` is not available right now η εντολή `{id}` δεν είναι διαθέσιμη αυτήν τη στιγμή @msg compound paths can only contain paths οι σύνθετες διαδρομές μπορούν να περιέχουν μόνο διαδρομές @msg compressed (RLE) bitmaps can't be read yet τα συμπιεσμένα (RLE) bitmap δεν μπορούν ακόμη να διαβαστούν @msg damaged κατεστραμμένο diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 10c0c0bf87..6f5922f096 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4395,6 +4395,7 @@ axis Both Ambos @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` no es un color como "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: como máximo {MAX_TABLE} colores @msg command `{0}` is not available right now: {1} el comando `{0}` no está disponible ahora: {1} +@msg command `{id}` is not available right now el comando `{id}` no está disponible ahora @msg compound paths can only contain paths los trazados compuestos solo pueden contener trazados @msg compressed (RLE) bitmaps can't be read yet todavía no se pueden leer mapas de bits comprimidos (RLE) @msg damaged dañado diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index 7845235f65..6f6d7ec436 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -4612,6 +4612,7 @@ imageTrace Outline Ääriviiva @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` ei ole väri, kuten "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: enintään {MAX_TABLE} väriä @msg command `{0}` is not available right now: {1} komento `{0}` ei ole nyt käytettävissä: {1} +@msg command `{id}` is not available right now komento `{id}` ei ole nyt käytettävissä @msg compound paths can only contain paths yhdistelmäpolut voivat sisältää vain polkuja @msg compressed (RLE) bitmaps can't be read yet pakattuja (RLE) bittikarttoja ei voi vielä lukea @msg damaged vaurioitunut diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 5e2895f9c2..4eacb054dc 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -4384,6 +4384,7 @@ axis Both Les deux @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what} : `{h}` n’est pas une couleur comme "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what} : {MAX_TABLE} couleurs au maximum @msg command `{0}` is not available right now: {1} la commande `{0}` n’est pas disponible pour le moment : {1} +@msg command `{id}` is not available right now la commande `{id}` n’est pas disponible pour le moment @msg compound paths can only contain paths les tracés transparents ne peuvent contenir que des tracés @msg compressed (RLE) bitmaps can't be read yet les images bitmap compressées (RLE) ne peuvent pas encore être lues @msg damaged endommagé diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index aca21d7eec..b8023bf0be 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -6273,6 +6273,7 @@ antiAlias Sharp शार्प @msg columns {first} and {column} both name the variable `{name}` कॉलम {first} और {column} दोनों नाम चर `{name}` @msg Combine Slices: the slices have no bounds स्लाइस को मिलाएं: स्लाइस में कोई सीमा नहीं है @msg command `{0}` is not available right now: {1} आदेश `{0}` अभी उपलब्ध नहीं है: {1} +@msg command `{id}` is not available right now आदेश `{id}` अभी उपलब्ध नहीं है @msg command must be a shape.* command command: आदेश एक आकार होना चाहिए।* आदेश @msg commands[{i}]: missing `command` आदेश [{i}]: लापता `command` @msg composer must be "singleLine" or "everyLine" composer: संगीतकार "singleLine" या "everyLine" होना चाहिए। diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 45d19b0ca9..e7e0f56e8d 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4946,6 +4946,7 @@ imageTrace Outline Körvonal @msg column must be true or a number from 1 a column értéke true vagy 1-től kezdődő szám lehet @msg Combine Slices: the slices have no bounds Szeletek egyesítése: a szeleteknek nincsenek határai @msg command `{0}` is not available right now: {1} a parancs jelenleg nem érhető el: `{0}`: {1} +@msg command `{id}` is not available right now a parancs jelenleg nem érhető el: `{id}` @msg compound paths can only contain paths összetett útvonal csak útvonalakat tartalmazhat @msg compressed (RLE) bitmaps can't be read yet a tömörített (RLE) bitképek még nem olvashatók @msg Convert to Artboards: selection has no area Átalakítás rajztáblákká: a kijelölésnek nincs területe diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 8e2b383e2d..0096d3ba9a 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -4351,6 +4351,7 @@ imageTrace Outline Kontur @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` bukan warna seperti "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: maksimal {MAX_TABLE} warna @msg command `{0}` is not available right now: {1} perintah `{0}` tidak tersedia saat ini: {1} +@msg command `{id}` is not available right now perintah `{id}` tidak tersedia saat ini @msg compound paths can only contain paths jalur majemuk hanya dapat berisi jalur @msg compressed (RLE) bitmaps can't be read yet bitmap terkompresi (RLE) belum dapat dibaca @msg damaged rusak diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 26e1f29651..853a7a3304 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4446,6 +4446,7 @@ axis Both Entrambi @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` non è un colore come "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: al massimo {MAX_TABLE} colori @msg command `{0}` is not available right now: {1} il comando `{0}` non è disponibile in questo momento: {1} +@msg command `{id}` is not available right now il comando `{id}` non è disponibile in questo momento @msg compound paths can only contain paths i tracciati composti possono contenere solo tracciati @msg compressed (RLE) bitmaps can't be read yet le bitmap compresse (RLE) non possono ancora essere lette @msg damaged danneggiato diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 30873028e8..2cbcf2d989 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -4580,6 +4580,7 @@ swatch Bright Violet ビビッドバイオレット @msg nothing to distort 変形するものがありません @msg unknown option `{path}.{k}` `{path}.{k}` は不明なオプションです @msg command `{0}` is not available right now: {1} コマンド `{0}` は現在使用できません: {1} +@msg command `{id}` is not available right now コマンド `{id}` は現在使用できません @msg internal error in `{cmd}`: {msg} (the document was left as it was before; please report this bug) `{cmd}` で内部エラーが発生しました: {msg} (ドキュメントは変更されていません。このバグを報告してください) @msg invalid parameters for `{cmd}`: {msg} `{cmd}` のパラメーターが正しくありません: {msg} @msg no active document アクティブなドキュメントがありません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 8e9d9d049d..004c9e044e 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -4166,6 +4166,7 @@ font-technology Variable ცვლადი @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` ფერი არ არის, მაგალითად "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: არაუმეტეს {MAX_TABLE} ფერი @msg command `{0}` is not available right now: {1} ბრძანება `{0}` ახლა ხელმისაწვდომი არ არის: {1} +@msg command `{id}` is not available right now ბრძანება `{id}` ახლა ხელმისაწვდომი არ არის @msg compound paths can only contain paths შედგენილი ტრაექტორია მხოლოდ ტრაექტორიებს შეიძლება შეიცავდეს @msg compressed (RLE) bitmaps can't be read yet შეკუმშული (RLE) ბიტმაპების წაკითხვა ჯერ არ ხერხდება @msg damaged დაზიანებულია diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index f00e6cdeba..d705ad35af 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -4507,6 +4507,7 @@ theme Appearance 외관 @msg nothing to distort 왜곡할 항목이 없습니다 @msg unknown option `{path}.{k}` 알 수 없는 옵션 `{path}.{k}` @msg command `{0}` is not available right now: {1} 명령 `{0}`은(는) 지금 사용할 수 없습니다: {1} +@msg command `{id}` is not available right now 명령 `{id}`은(는) 지금 사용할 수 없습니다 @msg internal error in `{cmd}`: {msg} (the document was left as it was before; please report this bug) `{cmd}`에서 내부 오류가 발생했습니다: {msg}(문서는 이전 상태로 유지되었습니다. 이 버그를 신고해 주세요) @msg invalid parameters for `{cmd}`: {msg} `{cmd}`의 매개변수가 올바르지 않습니다: {msg} @msg no active document 활성 문서가 없습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index bc00b76698..3efd07cd8a 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -4365,6 +4365,7 @@ imageTrace Outline Contour @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` is geen kleur zoals "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: maximaal {MAX_TABLE} kleuren @msg command `{0}` is not available right now: {1} opdracht `{0}` is nu niet beschikbaar: {1} +@msg command `{id}` is not available right now opdracht `{id}` is nu niet beschikbaar @msg compound paths can only contain paths samengestelde paden kunnen alleen paden bevatten @msg compressed (RLE) bitmaps can't be read yet gecomprimeerde (RLE) bitmaps kunnen nog niet gelezen worden @msg damaged beschadigd diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 23940fb80f..18eb491f68 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -4527,6 +4527,7 @@ imageTrace Outline Kontur @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` nie jest kolorem w rodzaju "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: maksymalnie {MAX_TABLE} kolorów @msg command `{0}` is not available right now: {1} polecenie `{0}` jest teraz niedostępne: {1} +@msg command `{id}` is not available right now polecenie `{id}` jest teraz niedostępne @msg compound paths can only contain paths ścieżki złożone mogą zawierać tylko ścieżki @msg compressed (RLE) bitmaps can't be read yet skompresowanych bitmap (RLE) nie można jeszcze odczytać @msg damaged uszkodzony diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 32c4b4bcc0..3048c0ff24 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3935,6 +3935,7 @@ axis Both Ambos @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` não é uma cor como "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: no máximo {MAX_TABLE} cores @msg command `{0}` is not available right now: {1} o comando `{0}` não está disponível agora: {1} +@msg command `{id}` is not available right now o comando `{id}` não está disponível agora @msg compound paths can only contain paths traçados compostos só podem conter traçados @msg compressed (RLE) bitmaps can't be read yet bitmaps compactados (RLE) ainda não podem ser lidos @msg damaged danificado diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 15d3c3a380..f504d64991 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -4432,6 +4432,7 @@ axis Both Оба @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` не является цветом, например "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: не более {MAX_TABLE} цветов @msg command `{0}` is not available right now: {1} команда `{0}` сейчас недоступна: {1} +@msg command `{id}` is not available right now команда `{id}` сейчас недоступна @msg compound paths can only contain paths составные контуры могут содержать только контуры @msg compressed (RLE) bitmaps can't be read yet сжатые (RLE) растровые изображения пока не читаются @msg damaged повреждено diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 5ffc963fad..0da6b3a63a 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -4298,6 +4298,7 @@ axis Both Båda @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` är inte en färg som "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: högst {MAX_TABLE} färger @msg command `{0}` is not available right now: {1} kommandot `{0}` är inte tillgängligt just nu: {1} +@msg command `{id}` is not available right now kommandot `{id}` är inte tillgängligt just nu @msg compound paths can only contain paths sammansatta banor kan bara innehålla banor @msg compressed (RLE) bitmaps can't be read yet komprimerade (RLE) bitmappar kan inte läsas än @msg damaged skadad diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 86356d74ba..377bb4b198 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -4160,6 +4160,7 @@ axis Both Her İkisi @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}`, "#ff8800" gibi bir renk değil @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: en fazla {MAX_TABLE} renginde @msg command `{0}` is not available right now: {1} `{0}` komutu şu anda kullanılamıyor: {1} +@msg command `{id}` is not available right now `{id}` komutu şu anda kullanılamıyor @msg compound paths can only contain paths bileşik yollar yalnızca yollar içerebilir @msg compressed (RLE) bitmaps can't be read yet sıkıştırılmış (RLE) bitmapler henüz okunamıyor @msg damaged hasarlı diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index ebba0f5c19..6541fa1f36 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -4430,6 +4430,7 @@ axis Both Обидві @msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` не є кольором, наприклад "#ff8800" @msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: не більше {MAX_TABLE} кольорів @msg command `{0}` is not available right now: {1} команда `{0}` зараз недоступна: {1} +@msg command `{id}` is not available right now команда `{id}` зараз недоступна @msg compound paths can only contain paths складені контури можуть містити лише контури @msg compressed (RLE) bitmaps can't be read yet стислі (RLE) растрові зображення поки не читаються @msg damaged пошкоджено diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 0ec0fd4e40..e20382bc13 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -4603,6 +4603,7 @@ theme Appearance Giao diện @msg column must be true or a number from 1 column phải là true hoặc số từ 1 @msg Combine Slices: the slices have no bounds Kết hợp lát cắt: các lát cắt không có giới hạn @msg command `{0}` is not available right now: {1} lệnh `{0}` hiện không khả dụng: {1} +@msg command `{id}` is not available right now lệnh `{id}` hiện không khả dụng @msg compound paths can only contain paths đường dẫn phức chỉ có thể chứa đường dẫn @msg compressed (RLE) bitmaps can't be read yet bitmap nén (RLE) chưa thể đọc được @msg Convert to Artboards: selection has no area Chuyển thành bảng vẽ: vùng chọn không có diện tích diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 9fc1cdc4e8..88ba43b1f7 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -4284,6 +4284,7 @@ imageTrace Outline 轮廓 @msg nothing to distort 没有可扭曲的内容 @msg unknown option `{path}.{k}` 未知选项 `{path}.{k}` @msg command `{0}` is not available right now: {1} 命令 `{0}` 当前不可用:{1} +@msg command `{id}` is not available right now 命令 `{id}` 当前不可用 @msg internal error in `{cmd}`: {msg} (the document was left as it was before; please report this bug) `{cmd}` 中出现内部错误:{msg} (文档保持原样;请报告此错误) @msg invalid parameters for `{cmd}`: {msg} `{cmd}` 的参数无效:{msg} @msg no active document 没有活动文档 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 685f7887fb..bc235583d9 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2078,6 +2078,7 @@ file-filter Templates 範本 @msg nothing to distort 沒有可扭曲的內容 @msg unknown option `{path}.{k}` 未知的選項 `{path}.{k}` @msg command `{0}` is not available right now: {1} 指令 `{0}` 目前無法使用:{1} +@msg command `{id}` is not available right now 指令 `{id}` 目前無法使用 @msg internal error in `{cmd}`: {msg} (the document was left as it was before; please report this bug) `{cmd}` 發生內部錯誤:{msg} (文件維持原狀;請回報此錯誤) @msg invalid parameters for `{cmd}`: {msg} `{cmd}` 的參數無效:{msg} @msg no active document 沒有作用中的文件 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 5651a6ff34..26dd765d29 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -72,8 +72,7 @@ pub(crate) use invoke::graph_dialog; pub(crate) use invoke::with_held_keys; pub use invoke::{click_target, invoke, invoke_from_system_menu}; pub(crate) use labels::lists_plugin; -#[cfg(test)] -use labels::shows_a_name; +pub(crate) use labels::shows_a_name; pub use labels::{CONTEXT_LABELS, DYNAMIC_LABELS, display_label, dynamic_label, item_shortcut, menu_strings}; pub use run::{check_params, run_ui_command}; pub(crate) use run::{normalize_panel, opt_bool, opt_index, opt_number}; diff --git a/crates/ui-egui/src/menus/labels.rs b/crates/ui-egui/src/menus/labels.rs index 408e5f077a..b8829ac0a2 100644 --- a/crates/ui-egui/src/menus/labels.rs +++ b/crates/ui-egui/src/menus/labels.rs @@ -125,7 +125,7 @@ fn command_label(app: &VectorcraftApp, id: &str, label: &str) -> Option /// label: a recent file, a saved view, a font, a user library or preset, a custom workspace, a /// plug-in, an SVG filter, a script of the Scripts folder? Such names are shown as they are, never /// translated (a workspace the user calls "Layers" stays "Layers"). -pub(super) fn shows_a_name(id: &str, label: &str) -> bool { +pub(crate) fn shows_a_name(id: &str, label: &str) -> bool { const SLOTS: [&str; 8] = [ "file.openRecent", "view.goto", diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index 1a58c496b2..7b82af2e53 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -325,9 +325,10 @@ mod tests { for it in items { match it { menus::Item::Sub(_, children) => walk(children, out), - // Items that list an installed plug-in come and go as tests beside this one - // install plug-ins, and no shortcut or pick reads them. - menus::Item::Cmd(_, id, p) if !menus::lists_plugin(id, p) => out.push(it), + // Items that list an installed plug-in or show a name that is user data come and + // go as tests beside this one install plug-ins or add toolbars, and no shortcut or + // pick reads them. + menus::Item::Cmd(label, id, p) if !menus::lists_plugin(id, p) && !menus::shows_a_name(id, label) => out.push(it), _ => {} } } diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index a580b2255a..f3a201124b 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -658,13 +658,16 @@ fn menu_bar_items() -> &'static HashMap<&'static str, Vec<(&'static str, Value)> } /// The (label, params) of each item of the menus `tree`, by command id. Items that list an -/// installed plug-in ([`menus::lists_plugin`]) are left out: [`menu_bar_items`] is built once, -/// and a plug-in installed or removed later would leave it with a row the menu no longer shows. +/// installed plug-in ([`menus::lists_plugin`]) or show a name that is user data +/// ([`menus::shows_a_name`]: a custom toolbar, a script, a recent file, a saved selection) are left +/// out: [`menu_bar_items`] is built once, and such items come and go after that. fn bar_items(tree: &[(&'static str, Vec)]) -> HashMap<&'static str, Vec<(&'static str, Value)>> { fn walk(items: &[menus::Item], out: &mut HashMap<&'static str, Vec<(&'static str, Value)>>) { for it in items { match it { - menus::Item::Cmd(label, id, p) if !menus::lists_plugin(id, p) => out.entry(*id).or_default().push((*label, p.clone())), + menus::Item::Cmd(label, id, p) if !menus::lists_plugin(id, p) && !menus::shows_a_name(id, label) => { + out.entry(*id).or_default().push((*label, p.clone())) + } menus::Item::Sub(_, children) => walk(children, out), _ => {} } @@ -693,15 +696,21 @@ pub(crate) fn menu_target(id: &str) -> Option<(String, Value)> { /// What a command search pick, or a shortcut without params, of command `id` runs now: `Ok` with /// what a click on its menu bar item runs ([`menu_target`]), or `None` when no such item gives it -/// params. While that item is dimmed, no dialog opens for it: `Err` holds the status line that says -/// why the command can't run (Remove Background… with no image selected). +/// params. While that item is dimmed ([`menus::enabled`]), no dialog opens for it: `Err` holds the +/// status line ([`dimmed`]), as for Remove Background… with no image selected. pub(crate) fn menu_target_now(app: &VectorcraftApp, id: &str) -> Option> { let target = menu_target(id)?; - if menus::enabled(app, id) { - return Some(Ok(target)); - } - let why = (vectorcraft_engine::find_command(id)?.enabled)(&app.session).err()?; - Some(Err(vectorcraft_engine::EngineError::Disabled(id.to_string(), why).to_string())) + Some(if menus::enabled(app, id) { Ok(target) } else { dimmed(app, id) }) +} + +/// What [`menu_target_now`] gives for command `id` while its menu bar item is dimmed: the status +/// line, with the engine's reason when it gives one. The app's own rules ([`menus::enabled`]) dim +/// UI commands, and some commands the engine would run, without giving a reason. +fn dimmed(app: &VectorcraftApp, id: &str) -> Result<(String, Value), String> { + let Some(why) = vectorcraft_engine::find_command(id).and_then(|c| (c.enabled)(&app.session).err()) else { + return Err(format!("command `{id}` is not available right now")); + }; + Err(vectorcraft_engine::EngineError::Disabled(id.to_string(), why).to_string()) } /// Is `key` a command with no default shortcut that a key press can't run: no item of the menu @@ -2289,24 +2298,46 @@ mod tests { } /// The menu bar items a shortcut or a command search pick reads leave out items that list an - /// installed plug-in (a File › Open With importer, an Object › Plug-ins filter), which come and - /// go after the items are read; the other items stay. + /// installed plug-in (a File › Open With importer, an Object › Plug-ins filter) or show a name + /// that is user data (a custom toolbar, a script of the Scripts folder), which come and go after + /// the items are read; the other items stay. #[test] - fn menu_bar_items_leave_out_installed_plug_ins() { + fn menu_bar_items_leave_out_installed_plug_ins_and_user_data() { let tree = vec![( "File", vec![ menus::Item::Sub("Open With", vec![menus::Item::Cmd("Spins…", "file.openWith", json!({"importer": "plugin.test.spins"}))]), menus::Item::Cmd("Filter…", "plugin.dialog", json!({"id": "test.filter"})), + menus::Item::Cmd("Mine", "window.toolbar.custom", json!({"name": "Mine"})), + menus::Item::Cmd("tidy.js", crate::scripts_menu::COMMAND, json!({"path": "/scripts/tidy.js"})), menus::Item::Cmd("Clean Up…", "object.path.cleanUp", json!({"strayPoints": true})), ], )]; let items = bar_items(&tree); assert!(!items.contains_key("file.openWith"), "{items:?}"); assert!(!items.contains_key("plugin.dialog"), "{items:?}"); + assert!(!items.contains_key("window.toolbar.custom"), "{items:?}"); + assert!(!items.contains_key(crate::scripts_menu::COMMAND), "{items:?}"); assert_eq!(items.get("object.path.cleanUp"), Some(&vec![("Clean Up…", json!({"strayPoints": true}))])); } + /// While a menu item is dimmed, a pick or a shortcut gets a status line and no target: with the + /// engine's reason when it gives one, and without one when the app's own rules dim the item (an + /// Appearance command with nothing targeted, a UI command). + #[test] + fn a_dimmed_item_gives_a_status_line_with_or_without_a_reason() { + use vectorcraft_testkit::fixtures::Fixture; + let none = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default()); + assert!(!menus::enabled(&none, "edit.findReplace")); + assert_eq!(dimmed(&none, "edit.findReplace"), Err("command `edit.findReplace` is not available right now: no document open".into())); + let empty = VectorcraftApp::new(Fixture::Empty.session(), crate::Services::default()); + for id in ["appearance.addFill", "ui.cropImage"] { + assert!(!menus::enabled(&empty, id), "{id}"); + assert!(vectorcraft_engine::find_command(id).is_none_or(|c| (c.enabled)(&empty.session).is_ok()), "{id}: the engine gives no reason"); + assert_eq!(dimmed(&empty, id), Err(format!("command `{id}` is not available right now"))); + } + } + /// Every command the editor lists, run as a key press runs it on a document with one or with /// several objects selected, reports no missing param. #[test] diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index bdc6efbc52..785fca82e2 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -413,22 +413,32 @@ fn a_single_key_shortcut_without_a_dialog_runs_at_once() { } /// A key or a chord of a command whose menu item is dimmed opens no dialog: Remove Background… and -/// Object Mosaic… with a rectangle selected change nothing and say why they can't run. +/// Object Mosaic… with a rectangle selected change nothing and say why they can't run, and so does +/// Find and Replace… with no document open. #[test] fn a_shortcut_of_a_dimmed_command_opens_no_dialog_and_says_why() { let chord = vec![ egui::Event::ModifiersChanged(cmd() | Modifiers::ALT | Modifiers::SHIFT), egui::Event::Key { key: Key::F9, physical_key: None, pressed: true, repeat: false, modifiers: cmd() | Modifiers::ALT | Modifiers::SHIFT }, ]; - for id in ["addon.image.removeBackground", "object.createObjectMosaic"] { + for id in ["addon.image.removeBackground", "object.createObjectMosaic", "edit.findReplace"] { assert!(crate::shortcut_editor::menu_target(id).is_some(), "{id} has a dialog"); for (shortcut, events) in [("F", press_f()), ("Cmd+Alt+Shift+F9", chord.clone())] { - let mut app = app_with_shortcut(id, shortcut); + let mut app = if id == "edit.findReplace" { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + crate::keymap::set_override(&mut app.ui.shortcut_overrides, "view.screenMode", ""); + crate::keymap::set_override(&mut app.ui.shortcut_overrides, id, shortcut); + app.ui.status.clear(); + app + } else { + app_with_shortcut(id, shortcut) + }; assert!(!crate::menus::enabled(&app, id), "{id}"); - let before = undo_len(&app); + let before = app.session.active().map(|st| st.history.undo.len()); frame(&mut app, events); crate::shortcut_editor::sync(&crate::state::UiState::default()); - assert_eq!((open_dialog(&app), undo_len(&app)), (None, before), "{id} on {shortcut}: {}", app.ui.status); + let after = app.session.active().map(|st| st.history.undo.len()); + assert_eq!((open_dialog(&app), after), (None, before), "{id} on {shortcut}: {}", app.ui.status); assert!(app.ui.status.contains("not available"), "{id} on {shortcut}: {}", app.ui.status); } }