From 788d0f517f47535dce4eaf5c58735b6f34de2dfa Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 16:21:55 -0800 Subject: [PATCH 1/2] Actions: Insert Menu Item adds a menu command to an action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Actions panel menu's Insert Menu Item… opens a dialog that lists the menu items an action can hold. Find picks the one item that matches the words typed, or a click on a listed item picks it, and OK inserts its command as a step after the selected step, at the end of the selected action, or into the recording in progress, without running it. Playback runs the step as the menu does. Items that open a dialog, Object > Align, and the File, Window and Help menu items are not listed. `ui.action.insertMenuItem` does the same for agents and refuses a command no listed item runs. Insert Stop and Insert Menu Item refuse a step that would give an action more than 10,000 steps or its set a file larger than 8 MiB, the most Load Actions… reads, for actions and recordings alike. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/mcp/src/ui_catalog.rs | 8 +- crates/ui-egui/src/dialogs.rs | 2 + .../src/dialogs/action_insert_menu_item.rs | 248 ++++++++++++ crates/ui-egui/src/i18n/bg.tsv | 10 + crates/ui-egui/src/i18n/bs.tsv | 10 + crates/ui-egui/src/i18n/ca.tsv | 10 + crates/ui-egui/src/i18n/cs.tsv | 10 + crates/ui-egui/src/i18n/da.tsv | 10 + crates/ui-egui/src/i18n/de.tsv | 10 + crates/ui-egui/src/i18n/el.tsv | 10 + crates/ui-egui/src/i18n/es.tsv | 10 + crates/ui-egui/src/i18n/fi.tsv | 10 + crates/ui-egui/src/i18n/fr.tsv | 10 + crates/ui-egui/src/i18n/hi.tsv | 10 + crates/ui-egui/src/i18n/hu.tsv | 10 + crates/ui-egui/src/i18n/id.tsv | 10 + crates/ui-egui/src/i18n/it.tsv | 10 + crates/ui-egui/src/i18n/ja.tsv | 10 + crates/ui-egui/src/i18n/ka.tsv | 10 + crates/ui-egui/src/i18n/ko.tsv | 10 + crates/ui-egui/src/i18n/nl.tsv | 10 + crates/ui-egui/src/i18n/pl.tsv | 10 + crates/ui-egui/src/i18n/pt-br.tsv | 10 + crates/ui-egui/src/i18n/ru.tsv | 10 + crates/ui-egui/src/i18n/sv.tsv | 10 + crates/ui-egui/src/i18n/tr.tsv | 10 + crates/ui-egui/src/i18n/uk.tsv | 10 + crates/ui-egui/src/i18n/vi.tsv | 10 + crates/ui-egui/src/i18n/zh-hans.tsv | 10 + crates/ui-egui/src/i18n/zh-hant.tsv | 10 + crates/ui-egui/src/menus.rs | 4 +- crates/ui-egui/src/menus/entries.rs | 65 ++++ crates/ui-egui/src/menus/run.rs | 1 + crates/ui-egui/src/menus/ui_commands.rs | 8 +- crates/ui-egui/src/panels/actions.rs | 368 +++++++++++++++++- crates/ui-egui/src/panels/actions/files.rs | 2 +- docs/mcp.md | 16 +- docs/target-app-parity.md | 1 + 38 files changed, 976 insertions(+), 17 deletions(-) create mode 100644 crates/ui-egui/src/dialogs/action_insert_menu_item.rs diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index e15244580..9e4237622 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -1377,7 +1377,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "ui.action.insertStop", "Insert Stop", "", - "{set?: index, action?: index, step?: index, message?, allowContinue?: bool} insert a stop, at which a playing action waits and shows message. With set and action (0 is the top set or action), the stop goes into that action, after step, else after its last step; without them it goes into the recording in progress, after the steps recorded so far. message defaults to none, and allowContinue, which offers Continue in the message box, defaults to false. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, or no action and no recording is an error and changes nothing. The panel menu's Insert Stop… (dialog `actionStopRecord` with message, allowContinue, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", + "{set?: index, action?: index, step?: index, message?, allowContinue?: bool} insert a stop, at which a playing action waits and shows message. With set and action (0 is the top set or action), the stop goes into that action, after step, else after its last step; without them it goes into the recording in progress, after the steps recorded so far. message defaults to none, and allowContinue, which offers Continue in the message box, defaults to false. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, no action and no recording, or a stop that would give the action more than 10,000 steps or its set's file more than 8 MiB is an error and changes nothing. The panel menu's Insert Stop… (dialog `actionStopRecord` with message, allowContinue, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", + ), + ( + "ui.action.insertMenuItem", + "Insert Menu Item", + "", + "{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", ), ( "ui.action.continue", diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index c13a230a9..719a42d8d 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -8,6 +8,7 @@ mod about; pub mod action_batch; +pub mod action_insert_menu_item; pub mod action_new; pub mod action_playback; pub mod action_set_options; @@ -389,6 +390,7 @@ registry! { ActionPlayback: [action_playback::KIND] => action_playback::SPEC, ActionStopRecord: [action_stop::RECORD_KIND] => action_stop::RECORD_SPEC, ActionStopMessage: [action_stop::MESSAGE_KIND] => action_stop::MESSAGE_SPEC, + ActionInsertMenuItem: [action_insert_menu_item::KIND] => action_insert_menu_item::SPEC, SliceOptions: [slices::OPTIONS] => slices::OPTIONS_SPEC, DivideSlices: [slices::DIVIDE] => slices::DIVIDE_SPEC, EpsOptions: [eps_options::KIND] => eps_options::SPEC, diff --git a/crates/ui-egui/src/dialogs/action_insert_menu_item.rs b/crates/ui-egui/src/dialogs/action_insert_menu_item.rs new file mode 100644 index 000000000..77dafac83 --- /dev/null +++ b/crates/ui-egui/src/dialogs/action_insert_menu_item.rs @@ -0,0 +1,248 @@ +//! Actions panel menu › Insert Menu Item… (`ui.action.insertMenuItem`): the menu items that can be +//! steps of an action, to pick one from the list or find it by name. +//! +//! Its fields: `find` (the words the list is narrowed to), `command` and `params` (the item picked; +//! `command` is empty until one is), `note` (`none` or `several` when Find picked nothing), and +//! `set`, `action` and `step`, the place of the new step (no `set` and `action`: the recording in +//! progress). Picking an item runs nothing. OK inserts it; an invalid place is an error, and the +//! dialog stays open. + +use std::sync::Arc; + +use serde_json::{Value, json}; + +use super::DialogSpec; +use crate::state::Dialog; +use crate::theme::Tokens; +use crate::{VectorcraftApp, menus, widgets}; + +/// The dialog kind of Insert Menu Item. +pub const KIND: &str = "actionInsertMenuItem"; + +/// The most rows the list shows at once; Find narrows it. +const MAX_LISTED: usize = 200; + +/// The width of the Menu Item label. +const LABEL_W: f32 = 80.0; + +pub(super) const SPEC: DialogSpec = DialogSpec { + heading: |_| tl!("Insert Menu Item").into(), + body, + confirm, + ok_enabled: Some(|_, d| !d.str("command").is_empty()), + min_width: 460.0, + ..DialogSpec::FORM +}; + +/// Opens Insert Menu Item for action `target` as (set, action), after `step`, else for the recording +/// in progress. +pub fn open(app: &mut VectorcraftApp, target: Option<(usize, usize)>, step: Option) { + let mut fields = json!({"find": "", "command": "", "params": {}, "note": ""}); + if let Some((set, action)) = target { + fields["set"] = json!(set); + fields["action"] = json!(action); + if let Some(step) = step { + fields["step"] = json!(step); + } + } + app.ui.dialog = Some(Dialog::new(KIND, fields)); +} + +/// A row of the list: an item of [`menus::insertable_items`], with what it shows and what Find +/// matches worked out once. +pub(crate) struct Row { + command: &'static str, + params: Value, + /// The menu path and label in the UI language (`Object › Group`). + shown: String, + /// The English path and label, the shown one and the command id, lowercased, one per line. + haystack: String, +} + +/// The rows in `lang`, for menus that name fonts in English when `english_names` is set. +fn build(lang: crate::i18n::Lang, english_names: bool) -> Vec { + menus::insertable_items(english_names) + .into_iter() + .map(|item| { + let parts = || item.path.iter().chain(std::iter::once(&item.label)).copied(); + let english = parts().collect::>().join(" › "); + let shown = parts().map(|s| crate::i18n::tr(lang, s)).collect::>().join(" › "); + let haystack = format!("{}\n{}\n{}", english.to_lowercase(), shown.to_lowercase(), item.command.to_lowercase()); + Row { command: item.command, params: item.params, shown, haystack } + }) + .collect() +} + +/// The rows, built again only when the language, the installed plug-ins, the font name setting or +/// the loaded fonts change, not on every frame. +fn rows(app: &VectorcraftApp, ctx: &egui::Context) -> Arc> { + type Key = (&'static str, u64, bool, u64, bool); + let lang = crate::i18n::current(); + let english_names = app.session.prefs.font_names_in_english; + let db = vectorcraft_text::FontDb::global(); + // Read before building, as the Font menu does: fonts that load meanwhile build the rows again. + let (cataloged, generation) = (db.installed_fonts_ready(), db.generation()); + let key: Key = (lang.code(), menus::plugin_revision(), english_names, generation, cataloged); + let id = egui::Id::unique("action-insert-menu-item-rows"); + if let Some((k, v)) = ctx.data(|d| d.get_temp::<(Key, Arc>)>(id)) + && k == key + { + return v; + } + let built = Arc::new(build(lang, english_names)); + ctx.data_mut(|d| d.insert_temp::<(Key, Arc>)>(id, (key, built.clone()))); + built +} + +/// The rows that every word of `find` matches, in menu order. +fn matches<'a>(rows: &'a [Row], find: &str) -> impl Iterator { + let words: Vec = find.to_lowercase().split_whitespace().map(str::to_string).collect(); + rows.iter().filter(move |r| words.iter().all(|w| r.haystack.contains(w.as_str()))) +} + +/// Picks `row` in `d`. +fn pick(d: &mut Dialog, row: &Row) { + d.fields.insert("command".into(), json!(row.command)); + d.fields.insert("params".into(), row.params.clone()); + d.fields.insert("note".into(), json!("")); +} + +/// Find: picks the one row that `find` matches; with none or several, picks nothing and notes it. +fn find(rows: &[Row], d: &mut Dialog) { + let found: Vec<&Row> = matches(rows, &d.str("find")).take(2).collect(); + match found[..] { + [row] => pick(d, row), + [] => { + d.fields.insert("note".into(), json!("none")); + } + _ => { + d.fields.insert("note".into(), json!("several")); + } + } +} + +/// Whether `row` is the item picked in `d`. +fn picked(d: &Dialog, row: &Row) -> bool { + d.fields.get("command").and_then(Value::as_str) == Some(row.command) && d.fields.get("params") == Some(&row.params) +} + +fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { + let rows = rows(app, ui.ctx()); + let dim = Tokens::get(ui.ctx()).text_dim; + let mut find_now = false; + ui.horizontal(|ui| { + if super::form::text_named(ui, d, "find", tl!("Find"), 300.0) { + d.fields.insert("note".into(), json!("")); + } + find_now = widgets::secondary_button(ui, tl!("Find")).clicked(); + }); + if find_now { + find(&rows, d); + } + match d.str("note").as_str() { + "none" => { + ui.label(egui::RichText::new(tl!("No menu item matches.")).color(dim)); + } + "several" => { + ui.label(egui::RichText::new(tl!("Several menu items match. Choose one from the list.")).color(dim)); + } + _ => {} + } + ui.add_space(4.0); + let mut chosen: Option = None; + let query = d.str("find"); + egui::ScrollArea::vertical().max_height(260.0).auto_shrink([false, true]).show(ui, |ui| { + for (i, row) in matches(&rows, &query).enumerate().take(MAX_LISTED) { + if ui.selectable_label(picked(d, row), row.shown.as_str()).clicked() { + chosen = Some(i); + } + } + }); + if let Some(row) = chosen.and_then(|i| matches(&rows, &query).nth(i)) { + pick(d, row); + } + ui.add_space(4.0); + let shown = rows.iter().find(|r| picked(d, r)).map_or_else( + || { + let command = d.str("command"); + if command.is_empty() { tl!("None").to_string() } else { command } + }, + |r| r.shown.clone(), + ); + widgets::label_row(ui, "Menu Item:", LABEL_W, |ui| { + ui.label(shown); + }); + ui.label(egui::RichText::new(tl!("Items that open a dialog, and File, Window and Help menu items, are not listed.")).color(dim)); + false +} + +fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + let mut params = json!({"command": d.str("command"), "params": d.fields.get("params").cloned().unwrap_or_else(|| json!({}))}); + for key in ["set", "action", "step"] { + if let Some(value) = d.fields.get(key) { + params[key] = value.clone(); + } + } + let r = app.run(crate::panels::actions::INSERT_MENU_ITEM, params)?; + app.ui.dialog = None; + Ok(r) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::i18n::Lang; + + #[test] + fn find_picks_the_one_match_and_notes_none_or_several() { + let rows = build(Lang::EN, true); + let mut d = Dialog::new(KIND, json!({"find": "object group", "command": "", "params": {}, "note": ""})); + find(&rows, &mut d); + assert_eq!((d.str("note"), d.str("command")), ("several".to_string(), String::new()), "Group and Ungroup both match"); + d.fields.insert("find".into(), json!("OBJECT.UNGROUP")); + find(&rows, &mut d); + assert_eq!((d.str("command"), d.fields["params"].clone(), d.str("note")), ("object.ungroup".to_string(), json!({}), String::new())); + d.fields.insert("find".into(), json!("no such menu item here")); + find(&rows, &mut d); + assert_eq!((d.str("note"), d.str("command")), ("none".to_string(), "object.ungroup".to_string()), "a failed Find keeps the pick"); + } + + /// The list holds the menu items that run an engine command without asking for values first, + /// outside the application, File, Window and Help menus, and leaves out Undo, Redo, the Pastes + /// and the saved-selection, saved-view and recent-font items. + #[test] + fn the_list_holds_only_menu_items_that_can_be_steps() { + let items = menus::insertable_items(true); + assert!(items.len() > 100, "only {} items", items.len()); + for item in &items { + assert!(vectorcraft_engine::find_command(item.command).is_some(), "{item:?}"); + assert!(!item.label.ends_with('…'), "{item:?}"); + assert!(!["VectorCraft", "File", "Window", "Help"].contains(&item.path[0]), "{item:?}"); + assert!(item.params.is_object(), "{item:?}"); + } + let has = |command: &str| items.iter().any(|i| i.command == command); + for command in ["object.group", "object.ungroup", "object.arrange.bringToFront"] { + assert!(has(command), "{command}"); + } + for command in + ["edit.undo", "edit.redo", "edit.paste", "edit.pasteInFront", "object.move", "object.align", "file.save", "view.zoomIn", "select.recall1"] + { + assert!(!has(command), "{command}"); + } + } + + /// Rows show their menu path in the UI language, and Find matches the English path, the shown + /// one and the command id. + #[test] + fn rows_show_the_translated_path_and_match_english_shown_text_and_ids() { + let de = Lang::from_code("de").unwrap(); + let rows = build(de, true); + let group = rows.iter().find(|r| r.command == "object.group").unwrap(); + assert_eq!(group.shown, format!("{} › {}", crate::i18n::tr(de, "Object"), crate::i18n::tr(de, "Group"))); + for words in ["object group", group.shown.as_str(), "object.group"] { + assert!(matches(&rows, words).any(|r| r.command == "object.group"), "{words}"); + } + let shared = rows.iter().any(|r| rows.iter().any(|o| o.command == r.command && o.params != r.params)); + assert!(shared, "items that share a command are listed apart, by their params"); + } +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index d83b5ac3c..c40c98bbb 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -5871,6 +5871,16 @@ axis Y Axis Y ос @msg message must be text message трябва да бъде текст @msg allowContinue must be true or false allowContinue трябва да бъде true или false @msg Batch cannot play an action with a stop Пакет не може да изпълни действие със спиране + Insert Menu Item… Вмъкване на елемент от меню… + Insert Menu Item Вмъкване на елемент от меню + Menu Item: Елемент от меню: + No menu item matches. Няма съвпадащ елемент от меню. + Several menu items match. Choose one from the list. Съвпадат няколко елемента от меню. Изберете един от списъка. + Items that open a dialog, and File, Window and Help menu items, are not listed. Елементите, които отварят диалогов прозорец, и елементите от менютата „Файл“, „Прозорец“ и „Помощ“ не са изброени. +@msg command must be text command трябва да бъде текст +@msg no menu item that can be inserted runs this command няма елемент от меню за вмъкване, който изпълнява тази команда +@msg the action already has 10,000 steps действието вече има 10 000 стъпки +@msg the action set would be larger than 8 MiB наборът от действия ще стане по-голям от 8 MiB Playback Options Опции за възпроизвеждане Accelerated Ускорено Step by Step Стъпка по стъпка diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index 663e95cea..37ba317e7 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -5910,6 +5910,16 @@ axis Y Axis Y osa @msg message must be text message mora biti tekst @msg allowContinue must be true or false allowContinue mora biti true ili false @msg Batch cannot play an action with a stop Paketna obrada ne može reprodukovati akciju sa zaustavljanjem + Insert Menu Item… Umetni stavku menija… + Insert Menu Item Umetni stavku menija + Menu Item: Stavka menija: + No menu item matches. Nijedna stavka menija ne odgovara. + Several menu items match. Choose one from the list. Odgovara više stavki menija. Izaberite jednu sa liste. + Items that open a dialog, and File, Window and Help menu items, are not listed. Stavke koje otvaraju dijalog i stavke menija Datoteka, Prozor i Pomoć nisu navedene. +@msg command must be text command mora biti tekst +@msg no menu item that can be inserted runs this command nijedna stavka menija koja se može umetnuti ne pokreće ovu naredbu +@msg the action already has 10,000 steps akcija već ima 10.000 koraka +@msg the action set would be larger than 8 MiB skup akcija bio bi veći od 8 MiB Playback Options Opcije reprodukcije Accelerated Ubrzano Step by Step Korak po korak diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 9c60e87e0..c0f3ea84c 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -3934,6 +3934,16 @@ digits Malayalam Malaiàlam @msg message must be text message ha de ser text @msg allowContinue must be true or false allowContinue ha de ser true o false @msg Batch cannot play an action with a stop Lot no pot reproduir una acció amb una aturada + Insert Menu Item… Insereix un element de menú… + Insert Menu Item Insereix un element de menú + Menu Item: Element de menú: + No menu item matches. Cap element de menú no coincideix. + Several menu items match. Choose one from the list. Coincideixen diversos elements de menú. Trieu-ne un de la llista. + Items that open a dialog, and File, Window and Help menu items, are not listed. No es mostren els elements que obren un quadre de diàleg ni els elements dels menús Fitxer, Finestra i Ajuda. +@msg command must be text command ha de ser text +@msg no menu item that can be inserted runs this command cap element de menú que es pugui inserir no executa aquesta ordre +@msg the action already has 10,000 steps l’acció ja té 10.000 passos +@msg the action set would be larger than 8 MiB el conjunt d’accions superaria els 8 MiB Playback Options Opcions de reproducció Accelerated Accelerada Step by Step Pas a pas diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index e8f9666fa..7c69e2968 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -6095,6 +6095,16 @@ axis Y Axis osa Y @msg message must be text message musí být text @msg allowContinue must be true or false allowContinue musí být true nebo false @msg Batch cannot play an action with a stop Dávka nemůže přehrát akci se zastavením + Insert Menu Item… Vložit položku nabídky… + Insert Menu Item Vložit položku nabídky + Menu Item: Položka nabídky: + No menu item matches. Žádná položka nabídky neodpovídá. + Several menu items match. Choose one from the list. Odpovídá několik položek nabídky. Vyberte jednu ze seznamu. + Items that open a dialog, and File, Window and Help menu items, are not listed. Položky, které otevírají dialogové okno, a položky nabídek Soubor, Okno a Nápověda nejsou uvedeny. +@msg command must be text command musí být text +@msg no menu item that can be inserted runs this command tento příkaz nespouští žádná položka nabídky, kterou lze vložit +@msg the action already has 10,000 steps akce už má 10 000 kroků +@msg the action set would be larger than 8 MiB sada akcí by byla větší než 8 MiB Playback Options Možnosti přehrávání Accelerated Zrychleně Step by Step Krok za krokem diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 31915ed6b..4f3ecae57 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -5886,6 +5886,16 @@ axis Y Axis Y-akse @msg message must be text message skal være tekst @msg allowContinue must be true or false allowContinue skal være true eller false @msg Batch cannot play an action with a stop Batch kan ikke afspille en handling med et stop + Insert Menu Item… Indsæt menupunkt… + Insert Menu Item Indsæt menupunkt + Menu Item: Menupunkt: + No menu item matches. Intet menupunkt passer. + Several menu items match. Choose one from the list. Flere menupunkter passer. Vælg et fra listen. + Items that open a dialog, and File, Window and Help menu items, are not listed. Punkter, der åbner en dialogboks, og punkter i menuerne Filer, Vindue og Hjælp vises ikke. +@msg command must be text command skal være tekst +@msg no menu item that can be inserted runs this command intet menupunkt, der kan indsættes, kører denne kommando +@msg the action already has 10,000 steps handlingen har allerede 10.000 trin +@msg the action set would be larger than 8 MiB handlingssættet ville blive større end 8 MiB Playback Options Afspilningsindstillinger Accelerated Accelereret Step by Step Trin for trin diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 2c5424d81..c85d3bcbf 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -5678,6 +5678,16 @@ axis Y Axis Y-Achse @msg message must be text message muss Text sein @msg allowContinue must be true or false allowContinue muss true oder false sein @msg Batch cannot play an action with a stop Die Stapelverarbeitung kann keine Aktion mit einem Stopp abspielen + Insert Menu Item… Menübefehl einfügen… + Insert Menu Item Menübefehl einfügen + Menu Item: Menübefehl: + No menu item matches. Kein Menübefehl passt. + Several menu items match. Choose one from the list. Mehrere Menübefehle passen. Wählen Sie einen aus der Liste. + Items that open a dialog, and File, Window and Help menu items, are not listed. Befehle, die ein Dialogfeld öffnen, sowie Befehle der Menüs „Datei“, „Fenster“ und „Hilfe“ werden nicht aufgeführt. +@msg command must be text command muss Text sein +@msg no menu item that can be inserted runs this command kein einfügbarer Menübefehl führt diesen Befehl aus +@msg the action already has 10,000 steps die Aktion hat bereits 10.000 Schritte +@msg the action set would be larger than 8 MiB der Aktionssatz wäre größer als 8 MiB Playback Options Wiedergabeoptionen Accelerated Beschleunigt Step by Step Schrittweise diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 6071ed5be..0c90437c5 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -6046,6 +6046,16 @@ axis Y Axis Άξονας Y @msg message must be text Το message πρέπει να είναι κείμενο @msg allowContinue must be true or false Το allowContinue πρέπει να είναι true ή false @msg Batch cannot play an action with a stop Η μαζική επεξεργασία δεν μπορεί να αναπαράγει ενέργεια με διακοπή + Insert Menu Item… Εισαγωγή στοιχείου μενού… + Insert Menu Item Εισαγωγή στοιχείου μενού + Menu Item: Στοιχείο μενού: + No menu item matches. Κανένα στοιχείο μενού δεν ταιριάζει. + Several menu items match. Choose one from the list. Ταιριάζουν πολλά στοιχεία μενού. Επιλέξτε ένα από τη λίστα. + Items that open a dialog, and File, Window and Help menu items, are not listed. Τα στοιχεία που ανοίγουν παράθυρο διαλόγου και τα στοιχεία των μενού «Αρχείο», «Παράθυρο» και «Βοήθεια» δεν εμφανίζονται. +@msg command must be text Το command πρέπει να είναι κείμενο +@msg no menu item that can be inserted runs this command κανένα στοιχείο μενού που μπορεί να εισαχθεί δεν εκτελεί αυτή την εντολή +@msg the action already has 10,000 steps η ενέργεια έχει ήδη 10.000 βήματα +@msg the action set would be larger than 8 MiB το σύνολο ενεργειών θα ξεπερνούσε τα 8 MiB Playback Options Επιλογές αναπαραγωγής Accelerated Επιταχυμένα Step by Step Βήμα προς βήμα diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4a74e3ae3..e8753173f 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -6037,6 +6037,16 @@ axis Y Axis Eje Y @msg message must be text message debe ser texto @msg allowContinue must be true or false allowContinue debe ser true o false @msg Batch cannot play an action with a stop Lote no puede reproducir una acción con una parada + Insert Menu Item… Insertar elemento de menú… + Insert Menu Item Insertar elemento de menú + Menu Item: Elemento de menú: + No menu item matches. Ningún elemento de menú coincide. + Several menu items match. Choose one from the list. Coinciden varios elementos de menú. Elija uno de la lista. + Items that open a dialog, and File, Window and Help menu items, are not listed. No se muestran los elementos que abren un cuadro de diálogo ni los de los menús Archivo, Ventana y Ayuda. +@msg command must be text command debe ser texto +@msg no menu item that can be inserted runs this command ningún elemento de menú que se pueda insertar ejecuta este comando +@msg the action already has 10,000 steps la acción ya tiene 10 000 pasos +@msg the action set would be larger than 8 MiB el conjunto de acciones superaría los 8 MiB Playback Options Opciones de reproducción Accelerated Acelerada Step by Step Paso a paso diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index f2ec64f60..a46077533 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -6125,6 +6125,16 @@ axis Y Axis Y-akseli @msg message must be text message on oltava tekstiä @msg allowContinue must be true or false allowContinue on oltava true tai false @msg Batch cannot play an action with a stop Erä ei voi toistaa toimintoa, jossa on pysäytys + Insert Menu Item… Lisää valikkokohta… + Insert Menu Item Lisää valikkokohta + Menu Item: Valikkokohta: + No menu item matches. Mikään valikkokohta ei vastaa hakua. + Several menu items match. Choose one from the list. Useampi valikkokohta vastaa hakua. Valitse yksi luettelosta. + Items that open a dialog, and File, Window and Help menu items, are not listed. Valintaikkunan avaavia kohtia sekä Tiedosto-, Ikkuna- ja Ohje-valikkojen kohtia ei luetella. +@msg command must be text command on oltava tekstiä +@msg no menu item that can be inserted runs this command mikään lisättävissä oleva valikkokohta ei suorita tätä komentoa +@msg the action already has 10,000 steps toiminnossa on jo 10 000 vaihetta +@msg the action set would be larger than 8 MiB toimintosarja olisi suurempi kuin 8 MiB Playback Options Toistoasetukset Accelerated Nopeutettu Step by Step Vaihe kerrallaan diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 807b3363b..9f82ec414 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -6090,6 +6090,16 @@ axis Y Axis Axe Y @msg message must be text message doit être du texte @msg allowContinue must be true or false allowContinue doit valoir true ou false @msg Batch cannot play an action with a stop Le traitement par lots ne peut pas exécuter un script comportant un arrêt + Insert Menu Item… Insérer une commande de menu… + Insert Menu Item Insérer une commande de menu + Menu Item: Commande de menu : + No menu item matches. Aucune commande de menu ne correspond. + Several menu items match. Choose one from the list. Plusieurs commandes de menu correspondent. Choisissez-en une dans la liste. + Items that open a dialog, and File, Window and Help menu items, are not listed. Les commandes qui ouvrent une boîte de dialogue et celles des menus Fichier, Fenêtre et Aide ne sont pas listées. +@msg command must be text command doit être du texte +@msg no menu item that can be inserted runs this command aucune commande de menu insérable n’exécute cette commande +@msg the action already has 10,000 steps le script compte déjà 10 000 étapes +@msg the action set would be larger than 8 MiB le jeu de scripts dépasserait 8 Mio Playback Options Options d’exécution Accelerated Accélérée Step by Step Pas à pas diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 572a19779..55b7c4f9d 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3382,6 +3382,16 @@ antiAlias Sharp शार्प @msg message must be text message टेक्स्ट होना चाहिए @msg allowContinue must be true or false allowContinue true या false होना चाहिए @msg Batch cannot play an action with a stop बैच स्टॉप वाला एक्शन नहीं चला सकता + Insert Menu Item… मेनू आइटम जोड़ें… + Insert Menu Item मेनू आइटम जोड़ें + Menu Item: मेनू आइटम: + No menu item matches. कोई मेनू आइटम मेल नहीं खाता। + Several menu items match. Choose one from the list. कई मेनू आइटम मेल खाते हैं। सूची से एक चुनें। + Items that open a dialog, and File, Window and Help menu items, are not listed. डायलॉग खोलने वाले आइटम और फ़ाइल, विंडो और सहायता मेनू के आइटम सूचीबद्ध नहीं हैं। +@msg command must be text command टेक्स्ट होना चाहिए +@msg no menu item that can be inserted runs this command जोड़ा जा सकने वाला कोई मेनू आइटम यह कमांड नहीं चलाता +@msg the action already has 10,000 steps एक्शन में पहले से 10,000 कदम हैं +@msg the action set would be larger than 8 MiB एक्शन सेट 8 MiB से बड़ा हो जाएगा Playback Options प्लेबैक ऑप्शन्स Playback Options… प्लेबैक ऑप्शन्स… Plug-in Info प्लगइन जानकारी diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 8c2d2339b..b8b8d6f43 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -6078,6 +6078,16 @@ axis Y Axis Y tengely @msg message must be text message: szöveg kell @msg allowContinue must be true or false allowContinue: true vagy false kell @msg Batch cannot play an action with a stop A kötegelt feldolgozás nem játszhat le megállást tartalmazó műveletet + Insert Menu Item… Menüelem beszúrása… + Insert Menu Item Menüelem beszúrása + Menu Item: Menüelem: + No menu item matches. Egy menüelem sem egyezik. + Several menu items match. Choose one from the list. Több menüelem is egyezik. Válasszon egyet a listából. + Items that open a dialog, and File, Window and Help menu items, are not listed. A párbeszédablakot megnyitó elemek, valamint a Fájl, az Ablak és a Súgó menü elemei nem szerepelnek a listában. +@msg command must be text command: szöveg kell +@msg no menu item that can be inserted runs this command egyetlen beszúrható menüelem sem futtatja ezt a parancsot +@msg the action already has 10,000 steps a műveletnek már 10 000 lépése van +@msg the action set would be larger than 8 MiB a műveletkészlet nagyobb lenne 8 MiB-nál Playback Options Lejátszási beállítások Accelerated Gyorsított Step by Step Lépésenként diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 606ffae97..e7f635382 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -5886,6 +5886,16 @@ axis Y Axis Sumbu Y @msg message must be text message harus berupa teks @msg allowContinue must be true or false allowContinue harus true atau false @msg Batch cannot play an action with a stop Batch tidak dapat memutar tindakan dengan titik henti + Insert Menu Item… Sisipkan item menu… + Insert Menu Item Sisipkan item menu + Menu Item: Item menu: + No menu item matches. Tidak ada item menu yang cocok. + Several menu items match. Choose one from the list. Beberapa item menu cocok. Pilih salah satu dari daftar. + Items that open a dialog, and File, Window and Help menu items, are not listed. Item yang membuka dialog, serta item menu Berkas, Jendela, dan Bantuan, tidak dicantumkan. +@msg command must be text command harus berupa teks +@msg no menu item that can be inserted runs this command tidak ada item menu yang dapat disisipkan yang menjalankan perintah ini +@msg the action already has 10,000 steps tindakan sudah memiliki 10.000 langkah +@msg the action set would be larger than 8 MiB set tindakan akan lebih besar dari 8 MiB Playback Options Opsi pemutaran Accelerated Dipercepat Step by Step Langkah demi langkah diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index bf4c9e7b5..22d0860e1 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -5712,6 +5712,16 @@ axis Y Axis Asse Y @msg message must be text message deve essere testo @msg allowContinue must be true or false allowContinue deve essere true o false @msg Batch cannot play an action with a stop L'elaborazione batch non può eseguire un'azione con un'interruzione + Insert Menu Item… Inserisci voce di menu… + Insert Menu Item Inserisci voce di menu + Menu Item: Voce di menu: + No menu item matches. Nessuna voce di menu corrisponde. + Several menu items match. Choose one from the list. Corrispondono più voci di menu. Sceglierne una dall’elenco. + Items that open a dialog, and File, Window and Help menu items, are not listed. Le voci che aprono una finestra di dialogo e le voci dei menu File, Finestra e Aiuto non sono elencate. +@msg command must be text command deve essere testo +@msg no menu item that can be inserted runs this command nessuna voce di menu inseribile esegue questo comando +@msg the action already has 10,000 steps l’azione ha già 10.000 passaggi +@msg the action set would be larger than 8 MiB il set di azioni supererebbe 8 MiB Playback Options Opzioni di riproduzione Accelerated Accelerata Step by Step Passo per passo diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 7a12afcad..d1db04fb1 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -5777,6 +5777,16 @@ axis Y Axis Y 軸 @msg message must be text message にはテキストを指定してください @msg allowContinue must be true or false allowContinue には true または false を指定してください @msg Batch cannot play an action with a stop バッチでは停止を含むアクションを再生できません + Insert Menu Item… メニュー項目を挿入… + Insert Menu Item メニュー項目を挿入 + Menu Item: メニュー項目: + No menu item matches. 一致するメニュー項目がありません。 + Several menu items match. Choose one from the list. 複数のメニュー項目が一致します。リストから 1 つ選択してください。 + Items that open a dialog, and File, Window and Help menu items, are not listed. ダイアログを開く項目と、ファイル、ウィンドウ、ヘルプメニューの項目は表示されません。 +@msg command must be text command にはテキストを指定してください +@msg no menu item that can be inserted runs this command このコマンドを実行する、挿入できるメニュー項目はありません +@msg the action already has 10,000 steps アクションにはすでに 10,000 ステップがあります +@msg the action set would be larger than 8 MiB アクションセットが 8 MiB を超えます Playback Options 再生オプション Accelerated 高速 Step by Step ステップ実行 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index c58480c42..53319790c 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -6046,6 +6046,16 @@ axis Y Axis Y ღერძი @msg message must be text message უნდა იყოს ტექსტი @msg allowContinue must be true or false allowContinue უნდა იყოს true ან false @msg Batch cannot play an action with a stop პაკეტურ დამუშავებას არ შეუძლია გაჩერების მქონე მოქმედების შესრულება + Insert Menu Item… მენიუს ელემენტის ჩასმა… + Insert Menu Item მენიუს ელემენტის ჩასმა + Menu Item: მენიუს ელემენტი: + No menu item matches. მენიუს არცერთი ელემენტი არ ემთხვევა. + Several menu items match. Choose one from the list. ემთხვევა მენიუს რამდენიმე ელემენტი. აირჩიეთ ერთი სიიდან. + Items that open a dialog, and File, Window and Help menu items, are not listed. დიალოგის გამხსნელი ელემენტები და მენიუების „ფაილი“, „ფანჯარა“ და „დახმარება“ ელემენტები სიაში არ არის. +@msg command must be text command უნდა იყოს ტექსტი +@msg no menu item that can be inserted runs this command ჩასმადი მენიუს არცერთი ელემენტი არ უშვებს ამ ბრძანებას +@msg the action already has 10,000 steps მოქმედებას უკვე 10 000 ნაბიჯი აქვს +@msg the action set would be larger than 8 MiB მოქმედებების ნაკრები 8 MiB-ზე დიდი გახდებოდა Playback Options შესრულების პარამეტრები Accelerated დაჩქარებული Step by Step ნაბიჯ-ნაბიჯ diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 39efdecf2..ee7a77d8d 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -5673,6 +5673,16 @@ axis Y Axis Y축 @msg message must be text message의 값은 텍스트여야 합니다 @msg allowContinue must be true or false allowContinue의 값은 true 또는 false여야 합니다 @msg Batch cannot play an action with a stop 일괄 처리에서는 중지가 있는 액션을 재생할 수 없습니다 + Insert Menu Item… 메뉴 항목 삽입… + Insert Menu Item 메뉴 항목 삽입 + Menu Item: 메뉴 항목: + No menu item matches. 일치하는 메뉴 항목이 없습니다. + Several menu items match. Choose one from the list. 여러 메뉴 항목이 일치합니다. 목록에서 하나를 선택하세요. + Items that open a dialog, and File, Window and Help menu items, are not listed. 대화 상자를 여는 항목과 파일, 윈도우, 도움말 메뉴의 항목은 표시되지 않습니다. +@msg command must be text command의 값은 텍스트여야 합니다 +@msg no menu item that can be inserted runs this command 이 명령을 실행하는 삽입 가능한 메뉴 항목이 없습니다 +@msg the action already has 10,000 steps 액션에 이미 단계가 10,000개 있습니다 +@msg the action set would be larger than 8 MiB 액션 세트가 8 MiB를 넘게 됩니다 Playback Options 재생 옵션 Accelerated 가속 Step by Step 단계별 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index dd1d22edc..7c3d937b0 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -5937,6 +5937,16 @@ axis Y Axis Y-as @msg message must be text message moet tekst zijn @msg allowContinue must be true or false allowContinue moet true of false zijn @msg Batch cannot play an action with a stop Batch kan geen actie met een stop afspelen + Insert Menu Item… Menu-item invoegen… + Insert Menu Item Menu-item invoegen + Menu Item: Menu-item: + No menu item matches. Geen menu-item komt overeen. + Several menu items match. Choose one from the list. Meerdere menu-items komen overeen. Kies er een uit de lijst. + Items that open a dialog, and File, Window and Help menu items, are not listed. Items die een dialoogvenster openen en items uit de menu’s Bestand, Venster en Help worden niet weergegeven. +@msg command must be text command moet tekst zijn +@msg no menu item that can be inserted runs this command geen invoegbaar menu-item voert deze opdracht uit +@msg the action already has 10,000 steps de actie heeft al 10.000 stappen +@msg the action set would be larger than 8 MiB de actieset zou groter worden dan 8 MiB Playback Options Afspeelopties Accelerated Versneld Step by Step Stap voor stap diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index dd850a3a4..d645a7eaa 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -5949,6 +5949,16 @@ axis Y Axis Oś Y @msg message must be text message musi być tekstem @msg allowContinue must be true or false allowContinue musi mieć wartość true lub false @msg Batch cannot play an action with a stop Przetwarzanie wsadowe nie może odtworzyć operacji z zatrzymaniem + Insert Menu Item… Wstaw polecenie menu… + Insert Menu Item Wstaw polecenie menu + Menu Item: Polecenie menu: + No menu item matches. Żadne polecenie menu nie pasuje. + Several menu items match. Choose one from the list. Pasuje kilka poleceń menu. Wybierz jedno z listy. + Items that open a dialog, and File, Window and Help menu items, are not listed. Polecenia otwierające okno dialogowe oraz polecenia menu Plik, Okno i Pomoc nie są wyświetlane. +@msg command must be text command musi być tekstem +@msg no menu item that can be inserted runs this command żadne polecenie menu, które można wstawić, nie uruchamia tego polecenia +@msg the action already has 10,000 steps operacja ma już 10 000 kroków +@msg the action set would be larger than 8 MiB zestaw operacji byłby większy niż 8 MiB Playback Options Opcje odtwarzania Accelerated Przyspieszone Step by Step Krok po kroku diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index f67ae6cda..0894b021b 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -6010,6 +6010,16 @@ axis Y Axis Eixo Y @msg message must be text message deve ser texto @msg allowContinue must be true or false allowContinue deve ser true ou false @msg Batch cannot play an action with a stop Lote não pode reproduzir uma ação com uma parada + Insert Menu Item… Inserir Item de Menu… + Insert Menu Item Inserir Item de Menu + Menu Item: Item de menu: + No menu item matches. Nenhum item de menu corresponde. + Several menu items match. Choose one from the list. Vários itens de menu correspondem. Escolha um da lista. + Items that open a dialog, and File, Window and Help menu items, are not listed. Itens que abrem uma caixa de diálogo e itens dos menus Arquivo, Janela e Ajuda não são listados. +@msg command must be text command deve ser texto +@msg no menu item that can be inserted runs this command nenhum item de menu que possa ser inserido executa este comando +@msg the action already has 10,000 steps a ação já tem 10.000 etapas +@msg the action set would be larger than 8 MiB o conjunto de ações ficaria maior que 8 MiB Playback Options Opções de Reprodução Accelerated Acelerada Step by Step Passo a Passo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 8eb542582..cc3ea8326 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -5706,6 +5706,16 @@ axis Y Axis Ось Y @msg message must be text message должно быть текстом @msg allowContinue must be true or false allowContinue должно быть true или false @msg Batch cannot play an action with a stop Пакет не может воспроизвести действие с остановкой + Insert Menu Item… Вставить пункт меню… + Insert Menu Item Вставить пункт меню + Menu Item: Пункт меню: + No menu item matches. Нет подходящих пунктов меню. + Several menu items match. Choose one from the list. Подходит несколько пунктов меню. Выберите один из списка. + Items that open a dialog, and File, Window and Help menu items, are not listed. Пункты, открывающие диалоговое окно, и пункты меню «Файл», «Окно» и «Справка» не показаны. +@msg command must be text command должно быть текстом +@msg no menu item that can be inserted runs this command ни один пункт меню, который можно вставить, не выполняет эту команду +@msg the action already has 10,000 steps в действии уже 10 000 шагов +@msg the action set would be larger than 8 MiB набор действий стал бы больше 8 MiB Playback Options Параметры воспроизведения Accelerated Ускоренно Step by Step Пошагово diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index fb2c463de..564246a45 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -6118,6 +6118,16 @@ axis Y Axis Y-axel @msg message must be text message måste vara text @msg allowContinue must be true or false allowContinue måste vara true eller false @msg Batch cannot play an action with a stop Batch kan inte spela upp en åtgärd med ett stopp + Insert Menu Item… Infoga menyalternativ… + Insert Menu Item Infoga menyalternativ + Menu Item: Menyalternativ: + No menu item matches. Inget menyalternativ matchar. + Several menu items match. Choose one from the list. Flera menyalternativ matchar. Välj ett i listan. + Items that open a dialog, and File, Window and Help menu items, are not listed. Alternativ som öppnar en dialogruta och alternativ i menyerna Fil, Fönster och Hjälp visas inte. +@msg command must be text command måste vara text +@msg no menu item that can be inserted runs this command inget menyalternativ som kan infogas kör det här kommandot +@msg the action already has 10,000 steps åtgärden har redan 10 000 steg +@msg the action set would be larger than 8 MiB åtgärdsuppsättningen skulle bli större än 8 MiB Playback Options Uppspelningsalternativ Accelerated Accelererad Step by Step Steg för steg diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index d45a171cc..469fdb873 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -6051,6 +6051,16 @@ axis Y Axis Y ekseni @msg message must be text "message" metin olmalıdır @msg allowContinue must be true or false "allowContinue" true veya false olmalıdır @msg Batch cannot play an action with a stop Toplu işlem durak içeren bir eylemi oynatamaz + Insert Menu Item… Menü Öğesi Ekle… + Insert Menu Item Menü Öğesi Ekle + Menu Item: Menü öğesi: + No menu item matches. Eşleşen menü öğesi yok. + Several menu items match. Choose one from the list. Birden çok menü öğesi eşleşiyor. Listeden birini seçin. + Items that open a dialog, and File, Window and Help menu items, are not listed. İletişim kutusu açan öğeler ile Dosya, Pencere ve Yardım menülerinin öğeleri listelenmez. +@msg command must be text "command" metin olmalıdır +@msg no menu item that can be inserted runs this command eklenebilen hiçbir menü öğesi bu komutu çalıştırmaz +@msg the action already has 10,000 steps eylemde zaten 10.000 adım var +@msg the action set would be larger than 8 MiB eylem seti 8 MiB'tan büyük olurdu Playback Options Oynatma Seçenekleri Accelerated Hızlandırılmış Step by Step Adım Adım diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index acaf20b6b..d2ea92484 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -5692,6 +5692,16 @@ axis Y Axis Вісь Y @msg message must be text message має бути текстом @msg allowContinue must be true or false allowContinue має бути true або false @msg Batch cannot play an action with a stop Пакет не може відтворити дію із зупинкою + Insert Menu Item… Вставити пункт меню… + Insert Menu Item Вставити пункт меню + Menu Item: Пункт меню: + No menu item matches. Немає відповідних пунктів меню. + Several menu items match. Choose one from the list. Відповідає кілька пунктів меню. Виберіть один зі списку. + Items that open a dialog, and File, Window and Help menu items, are not listed. Пункти, що відкривають діалогове вікно, і пункти меню «Файл», «Вікно» та «Довідка» не показано. +@msg command must be text command має бути текстом +@msg no menu item that can be inserted runs this command жоден пункт меню, який можна вставити, не виконує цю команду +@msg the action already has 10,000 steps дія вже має 10 000 кроків +@msg the action set would be larger than 8 MiB набір дій став би більшим за 8 MiB Playback Options Параметри відтворення Accelerated Прискорено Step by Step Покроково diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 4a2bbd5a6..a47fbaa1c 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -5693,6 +5693,16 @@ axis Y Axis Trục Y @msg message must be text message phải là văn bản @msg allowContinue must be true or false allowContinue phải là true hoặc false @msg Batch cannot play an action with a stop Hàng loạt không thể phát hành động có điểm dừng + Insert Menu Item… Chèn mục menu… + Insert Menu Item Chèn mục menu + Menu Item: Mục menu: + No menu item matches. Không có mục menu nào khớp. + Several menu items match. Choose one from the list. Có nhiều mục menu khớp. Hãy chọn một mục trong danh sách. + Items that open a dialog, and File, Window and Help menu items, are not listed. Các mục mở hộp thoại và các mục trong menu Tệp, Cửa sổ và Trợ giúp không được liệt kê. +@msg command must be text command phải là văn bản +@msg no menu item that can be inserted runs this command không có mục menu nào có thể chèn chạy lệnh này +@msg the action already has 10,000 steps hành động đã có 10.000 bước +@msg the action set would be larger than 8 MiB bộ hành động sẽ lớn hơn 8 MiB Playback Options Tùy chọn phát lại Accelerated Tăng tốc Step by Step Từng bước diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 0eb7c7ebe..e9b2f1325 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -5933,6 +5933,16 @@ axis Y Axis Y 轴 @msg message must be text message 必须是文本 @msg allowContinue must be true or false allowContinue 必须是 true 或 false @msg Batch cannot play an action with a stop 批处理无法播放含有停止的动作 + Insert Menu Item… 插入菜单项… + Insert Menu Item 插入菜单项 + Menu Item: 菜单项: + No menu item matches. 没有匹配的菜单项。 + Several menu items match. Choose one from the list. 有多个菜单项匹配。请从列表中选择一个。 + Items that open a dialog, and File, Window and Help menu items, are not listed. 未列出会打开对话框的项目,以及“文件”“窗口”和“帮助”菜单中的项目。 +@msg command must be text command 必须是文本 +@msg no menu item that can be inserted runs this command 没有可插入的菜单项运行此命令 +@msg the action already has 10,000 steps 该动作已有 10,000 个步骤 +@msg the action set would be larger than 8 MiB 动作组将超过 8 MiB Playback Options 播放选项 Accelerated 加速 Step by Step 逐步 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 084a7e770..122b7e0c9 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -5730,6 +5730,16 @@ axis Y Axis Y 軸 @msg message must be text message 必須是文字 @msg allowContinue must be true or false allowContinue 必須是 true 或 false @msg Batch cannot play an action with a stop 批次無法播放含有停止的動作 + Insert Menu Item… 插入選單項目… + Insert Menu Item 插入選單項目 + Menu Item: 選單項目: + No menu item matches. 沒有相符的選單項目。 + Several menu items match. Choose one from the list. 有多個選單項目相符。請從清單中選擇一個。 + Items that open a dialog, and File, Window and Help menu items, are not listed. 未列出會開啟對話框的項目,以及「檔案」、「視窗」和「說明」選單中的項目。 +@msg command must be text command 必須是文字 +@msg no menu item that can be inserted runs this command 沒有可插入的選單項目執行此指令 +@msg the action already has 10,000 steps 這個動作已有 10,000 個步驟 +@msg the action set would be larger than 8 MiB 動作組會超過 8 MiB Playback Options 播放選項 Accelerated 加速 Step by Step 逐步 diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 3d3f2a8c0..2b24124e1 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -64,7 +64,9 @@ pub(crate) use bar::{home_key, home_showing}; #[cfg(test)] use bar::{menu_body, pointer_reaches_title}; pub use context::{context_items, context_menu_body, ruler_menu_body, ruler_unit_items}; -pub use entries::{MenuEntry, Row, context_entries, entry, menu_entries, pretty_shortcut, shown_shortcut}; +pub use entries::{ + InsertableItem, MenuEntry, Row, context_entries, entry, insertable, insertable_items, menu_entries, pretty_shortcut, shown_shortcut, +}; pub(crate) use invoke::graph_dialog; #[cfg(test)] pub(crate) use invoke::with_held_keys; diff --git a/crates/ui-egui/src/menus/entries.rs b/crates/ui-egui/src/menus/entries.rs index 815a00d6f..e2f564b1d 100644 --- a/crates/ui-egui/src/menus/entries.rs +++ b/crates/ui-egui/src/menus/entries.rs @@ -127,6 +127,71 @@ pub fn context_entries(app: &VectorcraftApp) -> Vec { out } +/// A menu item that the Actions panel's Insert Menu Item… adds to an action as a step +/// ([`insertable_items`]). +#[derive(Clone, Debug, PartialEq)] +pub struct InsertableItem { + /// The menu title and the submenus that hold the item, as the tree names them in English. + pub path: Vec<&'static str>, + /// The item's label in the tree, in English. + pub label: &'static str, + pub command: &'static str, + /// The item's params; `{}` for an item that has none. + pub params: Value, +} + +/// The menus whose items are not added to actions: the application menu, File (documents and +/// files), Window (panels and workspaces) and Help. +const NOT_INSERTED_MENUS: [&str; 4] = ["VectorCraft", "File", "Window", "Help"]; + +/// Whether the item labeled `label` in menu `title`, which runs command `id` with `p`, can be a step +/// of an action. It can when it runs an engine command, which action playback runs as the menu +/// does; its label does not end with "…", the mark of an item that asks for values first; it is not +/// in a menu of [`NOT_INSERTED_MENUS`]; and it is not Undo, Redo, a Paste, an item that stands for a +/// saved selection, a saved view or a recent font, a plug-in's item or an action's own command. Nor +/// is it Object › Align, whose menu items add the Align To choice and the selection's key to their +/// params when clicked ([`crate::panels::align::with_align_to`]), which playback does not. +pub fn insertable(title: &str, label: &str, id: &str, p: &Value) -> bool { + use super::slots::{RECENT_FONT_IDS, SAVED_SELECTION_IDS, SAVED_VIEW_IDS}; + vectorcraft_engine::find_command(id).is_some() + && !label.ends_with('…') + && !NOT_INSERTED_MENUS.contains(&title) + && !matches!(id, "edit.undo" | "edit.redo" | "command.batch" | "object.align") + && !id.starts_with("edit.paste") + && !id.starts_with("prefs.") + && !id.starts_with("action.") + && ![&RECENT_FONT_IDS[..], &SAVED_SELECTION_IDS[..], &SAVED_VIEW_IDS[..]].iter().any(|ids| ids.contains(&id)) + && !super::labels::lists_plugin(id, p) +} + +/// Every menu item that can be a step of an action ([`insertable`]), in menu order. An item that +/// shows in two places is listed in both. `english_names` is as [`menu_tree_named`] takes it. +pub fn insertable_items(english_names: bool) -> Vec { + fn walk(title: &'static str, path: &mut Vec<&'static str>, items: &[Item], out: &mut Vec) { + for it in items { + match it { + Item::Cmd(label, id, p) if insertable(title, label, id, p) => out.push(InsertableItem { + path: path.clone(), + label, + command: id, + params: if p.is_null() { serde_json::json!({}) } else { p.clone() }, + }), + Item::Sub(label, children) => { + path.push(label); + walk(title, path, children, out); + path.pop(); + } + _ => {} + } + } + } + let mut out = vec![]; + for (title, items) in menu_tree_named(english_names) { + walk(title, &mut vec![title], &items, &mut out); + } + out +} + fn flatten(app: &VectorcraftApp, path: Vec, items: &[Item], out: &mut Vec) { for it in items { match it { diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 1223e1e42..c3db35e54 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -896,6 +896,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::panels::actions::record(app, p), crate::panels::actions::STOP_RECORDING => crate::panels::actions::stop(app, p), crate::panels::actions::INSERT_STOP => crate::panels::actions::insert_stop(app, p), + crate::panels::actions::INSERT_MENU_ITEM => crate::panels::actions::insert_menu_item(app, p), crate::panels::actions::CONTINUE_PLAYBACK => crate::panels::actions::continue_playback(app, p), crate::panels::actions::DELETE_STEP => crate::panels::actions::delete_step(app, p), "ui.batchDialog" => { diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index a96f13e67..7d80e762e 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1378,7 +1378,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ crate::panels::actions::INSERT_STOP, "Insert Stop", "", - "{set?: index, action?: index, step?: index, message?, allowContinue?: bool} insert a stop, at which a playing action waits and shows message. With set and action (0 is the top set or action), the stop goes into that action, after step, else after its last step; without them it goes into the recording in progress, after the steps recorded so far. message defaults to none, and allowContinue, which offers Continue in the message box, defaults to false. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, or no action and no recording is an error and changes nothing. The panel menu's Insert Stop… (dialog `actionStopRecord` with message, allowContinue, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", + "{set?: index, action?: index, step?: index, message?, allowContinue?: bool} insert a stop, at which a playing action waits and shows message. With set and action (0 is the top set or action), the stop goes into that action, after step, else after its last step; without them it goes into the recording in progress, after the steps recorded so far. message defaults to none, and allowContinue, which offers Continue in the message box, defaults to false. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, no action and no recording, or a stop that would give the action more than 10,000 steps or its set's file more than 8 MiB is an error and changes nothing. The panel menu's Insert Stop… (dialog `actionStopRecord` with message, allowContinue, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", + ), + ( + crate::panels::actions::INSERT_MENU_ITEM, + "Insert Menu Item", + "", + "{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", ), ( crate::panels::actions::CONTINUE_PLAYBACK, diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 2533ef029..28f53fdf2 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -870,31 +870,103 @@ const SELECTED_STEP: &str = "actions-selected-step"; /// them, it goes into the recording in progress, after the steps recorded so far. `message` is /// the text playback shows (default: none), and `allowContinue` offers Continue in the message box /// (default: false). An index past the last set, action or step, a param of another type, params -/// that are not an object, or no action and no recording is an error, and then nothing changes. -/// Record Stop's OK runs this. A stop in an action becomes the selected step, and its action and set -/// become the selected ones. → {set, action, step}, or {recording: true} +/// that are not an object, or no action and no recording is an error, and then nothing changes. So +/// is a stop that would give the action more than 10,000 steps, or its set a file larger than 8 MiB, +/// the most Load Actions… reads. Record Stop's OK runs this. A stop in an action becomes the selected +/// step, and its action and set become the selected ones. → {set, action, step}, or {recording: true} pub fn insert_stop(app: &mut VectorcraftApp, p: &Value) -> Result { if !p.is_object() { return Err("`params` must be an object".into()); } let (message, allow_continue) = stop_params(p)?; - let step = (STOP_STEP.to_string(), json!({"message": message, "allowContinue": allow_continue})); + insert(app, p, (STOP_STEP.to_string(), json!({"message": message, "allowContinue": allow_continue}))) +} + +/// The command that inserts a menu item's command as a step ([`insert_menu_item`]). +pub const INSERT_MENU_ITEM: &str = "ui.action.insertMenuItem"; + +/// `ui.action.insertMenuItem {set?, action?, step?, command, params?}`: inserts the command of a +/// menu item as a step, without running it. `command` and `params` (default: `{}`) are those of a +/// menu item that can be a step ([`crate::menus::insertable_items`]); the step plays as the item +/// runs. The place is as [`insert_stop`] takes it, and the step is selected as a stop is. A command +/// and params that no such item has, a param of another type, params that are not an object, or a +/// place [`insert_stop`] refuses is an error, and then nothing changes. Insert Menu Item's OK runs +/// this. → {set, action, step}, or {recording: true} +pub fn insert_menu_item(app: &mut VectorcraftApp, p: &Value) -> Result { + if !p.is_object() { + return Err("`params` must be an object".into()); + } + let command = p.get("command").and_then(Value::as_str).ok_or("command must be text")?; + let params = match p.get("params") { + None => json!({}), + Some(v) if v.is_object() => v.clone(), + Some(_) => return Err("`params` must be an object".into()), + }; + let items = crate::menus::insertable_items(app.session.prefs.font_names_in_english); + if !items.iter().any(|item| item.command == command && item.params == params) { + return Err("no menu item that can be inserted runs this command".into()); + } + insert(app, p, (command.to_string(), params)) +} + +/// Err when adding `step` to the recording in progress would give the action that Stop +/// Playing/Recording keeps more than [`files::MAX_ACTION_STEPS`] steps, or its set a file larger +/// than [`files::MAX_SET_FILE`], the most Load Actions… reads. +fn recording_room(app: &VectorcraftApp, step: &(String, Value)) -> Result<(), String> { + let Some((set, name, start)) = app.ui.recording.clone() else { return Err("no such action".into()) }; + let start = app.session.journal.held().unwrap_or(start); + let mut steps = app.session.journal_for_action(start); + let into = app.ui.recording_into; + let holder = into.and_then(|id| app.ui.action_sets.iter().position(|s| s.actions.iter().any(|a| a.id == id))).unwrap_or(set); + let Some(mut s) = app.ui.action_sets.get(holder).cloned() else { return Ok(()) }; + let at = match into.and_then(|id| s.actions.iter().position(|a| a.id == id)) { + Some(at) => at, + None => { + s.actions.push(Action::new(name, vec![])); + s.actions.len() - 1 + } + }; + if s.actions[at].steps.len() + steps.len() >= files::MAX_ACTION_STEPS { + return Err("the action already has 10,000 steps".into()); + } + steps.push(step.clone()); + s.actions[at].steps.extend(steps); + if files::write_set(&s).len() > files::MAX_SET_FILE { + return Err("the action set would be larger than 8 MiB".into()); + } + Ok(()) +} + +/// Puts `step` where params `p` say ([`insert_stop`]) → what [`insert_stop`] returns. +fn insert(app: &mut VectorcraftApp, p: &Value, step: (String, Value)) -> Result { let index = |k: &str| p.get(k).and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok()); if p.get("set").is_none() && p.get("action").is_none() && p.get("step").is_none() { if app.ui.recording.is_none() { return Err("no such action".into()); } + recording_room(app, &step)?; app.session.journal.push(step); return Ok(json!({"recording": true})); } let (set, at) = index("set").zip(index("action")).ok_or("no such action")?; - let action = app.ui.action_sets.get_mut(set).and_then(|s| s.actions.get_mut(at)).ok_or("no such action")?; + let s = app.ui.action_sets.get_mut(set).filter(|s| at < s.actions.len()).ok_or("no such action")?; + let action = &mut s.actions[at]; let after = match p.get("step") { None => action.steps.len(), Some(_) => index("step").filter(|&s| s < action.steps.len()).ok_or("no such action step")? + 1, }; + if action.steps.len() >= files::MAX_ACTION_STEPS { + return Err("the action already has 10,000 steps".into()); + } + let excluded = action.excluded_steps.clone(); action.insert_step(after, step); let id = action.id; + if files::write_set(s).len() > files::MAX_SET_FILE { + let action = &mut s.actions[at]; + action.steps.remove(after); + action.excluded_steps = excluded; + return Err("the action set would be larger than 8 MiB".into()); + } app.ui.action_set = Some(set); app.ui.action_sets_selected = vec![set]; if let Some(ctx) = app.ui_ctx.clone() { @@ -922,8 +994,8 @@ pub const DELETE_STEP: &str = "ui.action.deleteStep"; /// `ui.action.deleteStep {set, action, step}`: deletes step `step`, a stop or a command, of the /// action at index `action` of set `set`. The steps after it keep whether they are excluded, and -/// no step of that action stays selected. A Record Stop dialog open on that step closes, and one -/// open on a later step of the action follows it up. A missing or invalid index is an error, and +/// no step of that action stays selected. A Record Stop or Insert Menu Item dialog open on that step +/// closes, and one open on a later step of the action follows it up. A missing or invalid index is an error, and /// then nothing changes. Undo does not bring the step back. Delete Selection runs this for the /// selected step. → {set, action, step} pub fn delete_step(app: &mut VectorcraftApp, p: &Value) -> Result { @@ -943,7 +1015,7 @@ pub fn delete_step(app: &mut VectorcraftApp, p: &Value) -> Result .ui .dialog .as_ref() - .filter(|d| d.kind == crate::dialogs::action_stop::RECORD_KIND) + .filter(|d| holds_step(d)) .filter(|d| d.fields.get("set").and_then(number) == Some(set) && d.fields.get("action").and_then(number) == Some(at)) .and_then(|d| d.fields.get("step").and_then(number)); if held == Some(step) { @@ -1190,10 +1262,16 @@ pub fn delete_sets(app: &mut VectorcraftApp, p: &Value) -> Result fn holds_set(d: &crate::state::Dialog) -> bool { d.kind == crate::dialogs::action_batch::KIND || d.kind == crate::dialogs::action_new::KIND - || d.kind == crate::dialogs::action_stop::RECORD_KIND + || holds_step(d) || (d.kind == crate::dialogs::action_set_options::KIND && d.fields.contains_key("set")) } +/// Whether dialog `d` is a Record Stop or Insert Menu Item dialog, which holds the place of a new +/// step in its `set`, `action` and `step` fields. +fn holds_step(d: &crate::state::Dialog) -> bool { + d.kind == crate::dialogs::action_stop::RECORD_KIND || d.kind == crate::dialogs::action_insert_menu_item::KIND +} + /// The command that duplicates an action or an action set ([`duplicate`]). pub const DUPLICATE: &str = "ui.actionSet.duplicate"; @@ -1240,9 +1318,9 @@ pub fn duplicate(app: &mut VectorcraftApp, p: &Value) -> Result { let Some(mut copy) = src.actions.get(a).map(Action::copy) else { return Err("no such action".into()) }; copy.name = format!("{} copy", copy.name); src.actions.insert(a + 1, copy); - // A Batch or Record Stop dialog open on a later action of the set follows it down. - if let Some(d) = - app.ui.dialog.as_mut().filter(|d| d.kind == crate::dialogs::action_batch::KIND || d.kind == crate::dialogs::action_stop::RECORD_KIND) + // A Batch, Record Stop or Insert Menu Item dialog open on a later action of the set follows it + // down. + if let Some(d) = app.ui.dialog.as_mut().filter(|d| d.kind == crate::dialogs::action_batch::KIND || holds_step(d)) && d.fields.get("set").and_then(index) == Some(set) && let Some(da) = d.fields.get("action").and_then(index).filter(|&da| da > a) { @@ -1317,6 +1395,10 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let step = selected_step(app, ui.ctx(), selected); crate::dialogs::action_stop::open_record(app, if recording { None } else { selected }, step); } + if widgets::menu_item(ui, tl!("Insert Menu Item…"), recording || has, false) { + let step = selected_step(app, ui.ctx(), selected); + crate::dialogs::action_insert_menu_item::open(app, if recording { None } else { selected }, step); + } if widgets::menu_item(ui, tl!("Action Options…"), has, false) && let Some((s, a)) = selected { @@ -3963,6 +4045,200 @@ mod tests { assert!(app.ui.action_sets[0].actions.last().unwrap().steps.is_empty(), "the refused stop is not recorded"); } + #[test] + fn insert_menu_item_goes_after_the_given_step_or_at_the_end_runs_nothing_and_plays_as_the_menu_runs_it() { + let (mut app, x) = app_with_three_moves(); + app.ui.action_sets[0].actions[0].excluded_steps = BTreeSet::from([0, 2]); + let doc = app.session.active().unwrap().doc.clone(); + let journal = app.session.journal.len(); + let r = app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "step": 0, "command": "object.group"})).unwrap(); + assert_eq!(r, json!({"set": 0, "action": 0, "step": 1})); + let r = app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "command": "type.changeCase", "params": {"case": "upper"}})).unwrap(); + assert_eq!(r, json!({"set": 0, "action": 0, "step": 4})); + assert!(std::sync::Arc::ptr_eq(&app.session.active().unwrap().doc, &doc), "inserting runs nothing"); + assert_eq!(app.session.journal.len(), journal, "and records nothing"); + let action = &app.ui.action_sets[0].actions[0]; + let ids: Vec<&str> = action.steps.iter().map(|(c, _)| c.as_str()).collect(); + assert_eq!(ids, ["object.move", "object.group", "object.move", "object.move", "type.changeCase"]); + assert_eq!((&action.steps[1].1, &action.steps[4].1), (&json!({}), &json!({"case": "upper"}))); + assert_eq!(action.excluded_steps, BTreeSet::from([0, 3]), "the step excluded after the insertion point moved with it"); + + app.ui.action_sets[0].actions[0].excluded_steps.insert(4); + let before = undo_steps(&app); + let r = play(&mut app, 0, 0).unwrap(); + let results = r["results"].as_array().unwrap(); + assert_eq!(results.len(), 2, "the group and the included move play, and the excluded change of case does not: {r}"); + assert!(results[0]["id"].is_u64(), "the group step made a group: {r}"); + assert_eq!(undo_steps(&app), before + 1, "the action plays as one undo step"); + assert_eq!(x(&app), 2.0, "the move of 2 moved the group"); + } + + #[test] + fn an_inserted_menu_item_plays_step_by_step() { + let (mut app, x) = app_with_three_moves(); + app.ui.action_sets[0].actions[0].steps.truncate(1); + app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "step": 0, "command": "object.group"})).unwrap(); + app.run(PLAYBACK_OPTIONS, json!({"speed": "step"})).unwrap(); + let rect = app.session.active().unwrap().selection.objects.first().copied(); + let before = undo_steps(&app); + let r = play(&mut app, 0, 0).unwrap(); + assert_eq!(r["pending"], json!(1), "the move ran, the group waits: {r}"); + play_out(&mut app); + assert_eq!(undo_steps(&app), before + 2, "each step its own undo step"); + assert_eq!(x(&app), 1.0); + let selected = app.session.active().unwrap().selection.objects.first().copied(); + assert!(selected.is_some() && selected != rect, "the group step ran and selected the new group"); + } + + #[test] + fn insert_menu_item_refuses_what_no_menu_item_runs_and_changes_nothing() { + let (mut app, _) = app_with_three_moves(); + let place = |extra: Value| { + let mut p = json!({"set": 0, "action": 0}); + if let (Some(p), Some(extra)) = (p.as_object_mut(), extra.as_object()) { + p.extend(extra.clone()); + } + p + }; + let none = "no menu item that can be inserted runs this command"; + for (params, want) in [ + (place(json!({})), "command must be text"), + (place(json!({"command": 5})), "command must be text"), + (place(json!({"command": "no.such.command"})), none), + // Undo, a File menu item, an item that asks for values first, the app's own commands and + // an action's own command. + (place(json!({"command": "edit.undo"})), none), + (place(json!({"command": "file.save"})), none), + (place(json!({"command": "object.move"})), none), + (place(json!({"command": "object.move", "params": {"dx": 1, "dy": 0}})), none), + (place(json!({"command": "view.zoomIn"})), none), + (place(json!({"command": INSERT_MENU_ITEM})), none), + (place(json!({"command": "edit.paste"})), none), + // Align, whose menu items add the Align To choice when clicked. + (place(json!({"command": "object.align", "params": {"horizontal": "left"}})), none), + // Params that no menu item gives its command. + (place(json!({"command": "type.changeCase"})), none), + (place(json!({"command": "type.changeCase", "params": {"case": "middle"}})), none), + (place(json!({"command": "object.group", "params": {"ids": [1]}})), none), + (place(json!({"command": "object.group", "params": [1]})), "`params` must be an object"), + (place(json!({"command": "object.group", "params": null})), "`params` must be an object"), + (place(json!({"command": "object.group", "step": 3})), "no such action step"), + (place(json!({"command": "object.group", "step": null})), "no such action step"), + (json!({"command": "object.group"}), "no such action"), + (json!({"command": "object.group", "set": 0}), "no such action"), + (json!({"command": "object.group", "set": 0, "action": 1}), "no such action"), + (json!({"command": "object.group", "set": 1, "action": 0}), "no such action"), + (json!(5), "`params` must be an object"), + (json!(null), "`params` must be an object"), + ] { + assert_eq!(app.run(INSERT_MENU_ITEM, params.clone()).unwrap_err(), want, "{params}"); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 3, "{params}"); + } + } + + /// A listed menu item runs from the menu what its step runs in playback: the click opens no + /// dialog, and it journals the item's own command with the item's own params. + #[test] + fn every_listed_menu_item_runs_from_the_menu_what_its_step_holds() { + for item in crate::menus::insertable_items(false) { + let (mut app, _) = app_with_three_moves(); + let start = app.session.journal.len(); + crate::menus::invoke(&mut app, item.command, item.params.clone()); + assert!(app.ui.dialog.is_none(), "{item:?} opens a dialog"); + let ran = app.session.journal_for_action(start); + assert!(ran.iter().all(|(c, p)| c != item.command || *p == item.params), "{item:?} ran {ran:?}"); + } + } + + #[test] + fn insert_menu_item_goes_into_the_recording() { + let (mut app, _) = app_with_three_moves(); + app.run(START_RECORDING, json!({})).unwrap(); + assert_eq!(app.run(INSERT_MENU_ITEM, json!({"command": "object.group"})).unwrap(), json!({"recording": true})); + assert_eq!(app.run(INSERT_MENU_ITEM, json!({"command": "file.save"})).unwrap_err(), "no menu item that can be inserted runs this command"); + app.run(STOP_RECORDING, json!({})).unwrap(); + let recorded = app.ui.action_sets[0].actions.last().unwrap(); + assert_eq!(recorded.steps, [("object.group".to_string(), json!({}))]); + } + + /// An action stays one that Load Actions… reads back: a stop or a menu item that would give it + /// more than 10,000 steps, or its set's file more than 8 MiB, is refused, and the action keeps its + /// steps and exclusions as they were. + #[test] + fn inserted_steps_keep_an_action_set_that_load_actions_reads() { + let (mut app, _) = app_with_three_moves(); + let full = vec![("object.group".to_string(), json!({})); files::MAX_ACTION_STEPS]; + app.ui.action_sets[0].actions[0].steps = full; + let steps = "the action already has 10,000 steps"; + assert_eq!(app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "command": "object.group"})).unwrap_err(), steps); + assert_eq!(app.run(INSERT_STOP, json!({"set": 0, "action": 0, "step": 0})).unwrap_err(), steps); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), files::MAX_ACTION_STEPS); + app.ui.action_sets[0].actions[0].steps.pop(); + assert!(app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "command": "object.group"})).is_ok(), "the 10,000th step goes in"); + let saved = files::write_set(&app.ui.action_sets[0]); + assert_eq!(files::read_set(&saved, "Test").unwrap().actions[0].steps.len(), files::MAX_ACTION_STEPS, "and loads again"); + + let pad = |n: usize| vec![("object.move".to_string(), json!({"dx": 1, "dy": 0, "pad": "x".repeat(n)}))]; + app.ui.action_sets[0].actions[0].steps = pad(0); + app.ui.action_sets[0].actions[0].excluded_steps = BTreeSet::from([0, 7]); + let room = files::MAX_SET_FILE - files::write_set(&app.ui.action_sets[0]).len(); + app.ui.action_sets[0].actions[0].steps = pad(room - 8); + let size = "the action set would be larger than 8 MiB"; + assert_eq!(app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "step": 0, "command": "object.group"})).unwrap_err(), size); + assert_eq!(app.run(INSERT_STOP, json!({"set": 0, "action": 0})).unwrap_err(), size); + let action = &app.ui.action_sets[0].actions[0]; + assert_eq!((action.steps.len(), &action.excluded_steps), (1, &BTreeSet::from([0, 7])), "nothing changed"); + assert!(files::write_set(&app.ui.action_sets[0]).len() <= files::MAX_SET_FILE); + } + + /// The limits hold for a step inserted into the recording in progress too: they count the steps + /// recorded so far and, for New Action's recording, the action's own steps and its set. + #[test] + fn steps_inserted_into_a_recording_keep_an_action_set_that_load_actions_reads() { + let (mut app, _) = app_with_three_moves(); + app.run(START_RECORDING, json!({})).unwrap(); + app.run(INSERT_MENU_ITEM, json!({"command": "object.group"})).unwrap(); + for _ in 1..files::MAX_ACTION_STEPS - 1 { + app.session.journal.push(("object.group".to_string(), json!({}))); + } + let steps = "the action already has 10,000 steps"; + assert_eq!(app.run(INSERT_STOP, json!({})).unwrap(), json!({"recording": true}), "the 10,000th step goes in"); + assert_eq!(app.run(INSERT_MENU_ITEM, json!({"command": "object.group"})).unwrap_err(), steps); + assert_eq!(app.run(INSERT_STOP, json!({})).unwrap_err(), steps); + app.run(STOP_RECORDING, json!({})).unwrap(); + let saved = files::write_set(&app.ui.action_sets[0]); + assert_eq!(files::read_set(&saved, "Test").unwrap().actions.last().unwrap().steps.len(), files::MAX_ACTION_STEPS, "and loads again"); + + let (mut app, _) = app_with_three_moves(); + let pad = |n: usize| ("object.move".to_string(), json!({"dx": 1, "dy": 0, "pad": "x".repeat(n)})); + app.ui.action_sets[0].actions[0].steps = vec![pad(0)]; + let room = files::MAX_SET_FILE - files::write_set(&app.ui.action_sets[0]).len(); + app.ui.action_sets[0].actions[0].steps = vec![pad(room - 8)]; + let into = app.ui.action_sets[0].actions[0].id; + app.run(START_RECORDING, json!({})).unwrap(); + app.ui.recording_into = Some(into); + let size = "the action set would be larger than 8 MiB"; + assert_eq!(app.run(INSERT_MENU_ITEM, json!({"command": "object.group"})).unwrap_err(), size); + assert_eq!(app.run(INSERT_STOP, json!({})).unwrap_err(), size); + app.run(STOP_RECORDING, json!({})).unwrap(); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 1, "nothing was recorded into the action"); + assert!(files::write_set(&app.ui.action_sets[0]).len() <= files::MAX_SET_FILE); + } + + /// A step inserted from the menu saves and loads as the command and params it holds, with its + /// place and exclusion. + #[test] + fn an_inserted_menu_item_saves_and_loads() { + let (mut app, _) = app_with_three_moves(); + app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "step": 1, "command": "type.changeCase", "params": {"case": "title"}})).unwrap(); + app.ui.action_sets[0].actions[0].excluded_steps = BTreeSet::from([2]); + let loaded = files::read_set(&files::write_set(&app.ui.action_sets[0]), "Test").unwrap(); + let action = &loaded.actions[0]; + assert_eq!(action.steps, app.ui.action_sets[0].actions[0].steps); + assert_eq!(action.steps[2], ("type.changeCase".to_string(), json!({"case": "title"}))); + assert_eq!(action.excluded_steps, BTreeSet::from([2])); + } + #[test] fn delete_step_removes_one_step_and_moves_the_exclusions_up() { let (mut app, _) = app_with_a_stop(false); @@ -4004,6 +4280,22 @@ mod tests { assert_eq!(open_kind(&app), None, "deleting its step closes it"); } + #[test] + fn step_and_set_edits_keep_an_open_insert_menu_item_on_its_place() { + let (mut app, _) = app_with_three_moves(); + app.ui.action_sets[0].actions.insert(0, Action::new("First", vec![])); + crate::dialogs::action_insert_menu_item::open(&mut app, Some((0, 1)), Some(2)); + app.run(DUPLICATE, json!({"set": 0, "action": 0})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["action"], json!(2), "a copied action above moves it down"); + app.run(DELETE_STEP, json!({"set": 0, "action": 2, "step": 0})).unwrap(); + assert_eq!(app.ui.dialog.as_ref().unwrap().fields["step"], json!(1), "a deleted step above moves it up"); + app.run(DELETE_STEP, json!({"set": 0, "action": 2, "step": 1})).unwrap(); + assert_eq!(open_kind(&app), None, "deleting its step closes it"); + crate::dialogs::action_insert_menu_item::open(&mut app, Some((0, 0)), None); + app.run(DELETE_SETS, json!({"sets": [0]})).unwrap(); + assert_eq!(open_kind(&app), None, "deleting its set closes it"); + } + /// Selects the first step of "Three Moves" by clicking its row, with the action open. fn click_first_step(ctx: &egui::Context, app: &mut VectorcraftApp) { use super::super::tests_appearance::{click, frame_events}; @@ -4474,6 +4766,58 @@ mod tests { assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 3); } + /// The panel menu's Insert Menu Item… after the selected step: Find narrows the list, a click on + /// a row picks the item and runs nothing, Cancel inserts nothing, and OK inserts the pick after + /// the step. With nothing selected and nothing recording, the item is off. + #[test] + fn insert_menu_item_in_the_menu_picks_from_the_list_and_inserts_on_ok() { + use super::super::tests_appearance::{click, frame_events, text_rect}; + let (mut app, _) = app_with_three_moves(); + app.ui.action_set = Some(0); + let ctx = egui::Context::default(); + app.ui_ctx = Some(ctx.clone()); + let open = |ctx: &egui::Context, app: &mut VectorcraftApp| { + let texts = frame_events(ctx, app, vec![], menu); + let item = texts.iter().find(|(s, _)| s.trim() == "Insert Menu Item…").map(|(_, r)| *r).expect("Insert Menu Item… in the menu"); + click(ctx, app, item.center(), menu); + }; + open(&ctx, &mut app); + assert_eq!(open_kind(&app), None, "nothing selected and nothing recording: the item is off"); + + click_first_step(&ctx, &mut app); + open(&ctx, &mut app); + let d = app.ui.dialog.as_ref().expect("Insert Menu Item… opens its dialog"); + assert_eq!(d.kind, crate::dialogs::action_insert_menu_item::KIND); + assert_eq!((&d.fields["set"], &d.fields["action"], &d.fields["step"]), (&json!(0), &json!(0), &json!(0))); + if let Some(d) = app.ui.dialog.as_mut() { + d.fields.insert("find".into(), json!("object.ungroup")); + } + let draw = |app: &mut VectorcraftApp, ui: &mut egui::Ui| crate::dialogs::show(app, ui.ctx()); + let mut texts = vec![]; + for _ in 0..3 { + texts = frame_events(&ctx, &mut app, vec![], draw); + } + assert!(!texts.iter().any(|(s, _)| s == "Object › Group"), "Find narrows the list: {texts:?}"); + let doc = app.session.active().unwrap().doc.clone(); + click(&ctx, &mut app, text_rect(&texts, "Object › Ungroup").center(), draw); + let d = app.ui.dialog.as_ref().expect("a pick keeps the dialog open"); + assert_eq!((d.str("command"), &d.fields["params"]), ("object.ungroup".to_string(), &json!({}))); + assert!(std::sync::Arc::ptr_eq(&app.session.active().unwrap().doc, &doc), "a pick runs nothing"); + + crate::dialogs::cancel(&mut app); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), 3, "Cancel inserts nothing"); + + open(&ctx, &mut app); + if let Some(d) = app.ui.dialog.as_mut() { + d.fields.insert("command".into(), json!("object.ungroup")); + } + crate::dialogs::confirm(&mut app).unwrap(); + assert_eq!(open_kind(&app), None, "OK closes the dialog"); + let ids: Vec = app.ui.action_sets[0].actions[0].steps.iter().map(|(c, _)| c.clone()).collect(); + assert_eq!(ids, ["object.move", "object.ungroup", "object.move", "object.move"], "the pick goes after the selected step"); + assert_eq!(pstate::>(&ctx, SELECTED_STEP), Some((app.ui.action_sets[0].actions[0].id, 1)), "and is selected"); + } + #[test] fn set_edits_keep_an_open_record_stop_on_its_action() { let mut app = app_with_sets(); diff --git a/crates/ui-egui/src/panels/actions/files.rs b/crates/ui-egui/src/panels/actions/files.rs index f7df78ee9..2e1385a7d 100644 --- a/crates/ui-egui/src/panels/actions/files.rs +++ b/crates/ui-egui/src/panels/actions/files.rs @@ -34,7 +34,7 @@ pub(super) const MAX_SET_FILE: usize = 8 << 20; const MAX_SET_ACTIONS: usize = 1000; /// The most steps [`read_set`] reads in one action. -const MAX_ACTION_STEPS: usize = 10_000; +pub(super) const MAX_ACTION_STEPS: usize = 10_000; /// The most left-out steps that Load Actions… lists for an action set another app saved. const MAX_SKIPPED: usize = 1000; diff --git a/docs/mcp.md b/docs/mcp.md index 5826c5c8c..76589bbc6 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2625,8 +2625,22 @@ already walks Image Trace. Short agent entry points: false) offers Continue in the message box. An action with an empty message still waits at the stop, which is VectorCraft's own rule. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an - object, or no action and no recording is an error, and then nothing changes. The panel menu's Insert Stop… opens the `actionStopRecord` dialog + object, no action and no recording, or a stop that would give the action more than 10,000 steps or its set's file + more than 8 MiB is an error, and then nothing changes. The panel menu's Insert Stop… opens the `actionStopRecord` dialog (`message`, `allowContinue`, `set`, `action`, `step`), whose OK runs this. +- Actions: `ui.action.insertMenuItem {set?, action?, step?, command, params?}` inserts a menu item's command as a step + without running it. `command` and `params` (default: `{}`) must be those of an item in the menus that can be a step: + an item that runs a document command without opening a dialog first, outside the File, Window and Help menus, and + not Undo, Redo, a Paste command, Object › Align (its items add the Align To choice when clicked), a preference + toggle, an Actions command or a recent-font, saved-selection or saved-view slot. Disabled items are listed, and playback checks the step when it runs. With `set` and `action`, the + step goes into that action, after step `step`, else after its last step, becomes the panel's selected step, and the + command returns `{set, action, step}`. Without them it goes into the recording in progress and returns + `{recording: true}`. A command no such item runs, params that are not an object, an index past the last set, action + or step, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file + more than 8 MiB is an error, and then nothing changes. The panel menu's Insert Menu Item… opens the + `actionInsertMenuItem` dialog (`find`, `command`, `params`, `set`, `action`, `step`): typing in Find and pressing + Find picks the one item whose menu path, shown name or command id holds every word typed, or lists the matches, and + OK runs this. - Actions: `ui.action.continue {}` plays on from the stop the playing action waits at, on the document active now, and closes the stop's message box (`actionStopMessage`). The next step runs at once, or at Accelerated the steps up to the next stop as one undo step. It returns `{results, pending, atStop?}`. With no action waiting at a stop it is an diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b4c949f74..8b2b77a24 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -711,6 +711,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Actions panel: expand an action to show its recorded steps, then clear a step's checkmark to keep it in the action but skip it during playback. The action and set checkmarks include or exclude all their steps; a mixed checkmark includes all. Alt/Option-clicking a step checkmark keeps that step as it was and applies the opposite inclusion state to the other steps in its action. This follows the public [Exclude commands from an action](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/exclude-commands-from-actions.html) guide; the scope of the mixed-state and Alt-click behavior is VectorCraft's design. The exclusion survives UI restart, action duplication, and `.vcactions` save/load. Existing version 1 action set files load with every step included. - Actions panel: the panel menu's Playback Options… sets how actions play: Accelerated (the default) plays every step at once, Step by Step plays one step at a time with the canvas drawn after each, and Pause For waits the given number of seconds between steps ([Specify playback speed](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/specify-playback-speed.html); `ui.action.playbackOptions`). VectorCraft's own rules, which the page does not cover: the pause is a whole number of seconds from 1 to 60, the setting is kept with the UI preferences, a paced playback ends at Stop Playing/Recording or when a step comes due while another document is active, each step of a paced playback is its own undo step, and Batch… plays at full speed. - Actions panel: the panel menu's Insert Stop… opens Record Stop, with a Message box and Allow Continue, and adds a stop to the recording in progress, after the selected command, or at the end of the selected action ([Insert stops in actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-stops-in-actions.html); `ui.action.insertStop`). A playing action waits at the stop and shows the message so the user can do a manual task, and Play Current Selection plays the rest ([Illustrator CS5 help](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=459); `ui.action.continue`). With Allow Continue, the message box also has Continue. VectorCraft's own rules, which the pages do not cover: an empty message still waits, Stop Playing/Recording ends the playback and keeps the steps already played, the rest plays on the document active then, a stop waits at every playback speed, at Accelerated the steps between two stops are one undo step, Batch… refuses an action with an included stop, and Delete Selection on a selected step deletes that step, a stop or a command, without asking (`ui.action.deleteStep`). + - Actions panel: the panel menu's Insert Menu Item… adds a menu command to the recording in progress, after the selected step, or at the end of the selected action, without running it, and playback runs it as the menu does ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertMenuItem`). The dialog's Find box picks the one item that matches the words typed, and a click on a listed item picks it. VectorCraft's own rules, which the page does not cover: items that open a dialog, the Object › Align items and the File, Window and Help menu items are not listed, and an action holds at most 10,000 steps and its set file at most 8 MiB, for stops too. - Actions panel: Delete Selection deletes the selected action set with its actions and keeps the other sets ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.delete`). VectorCraft's own rules, which the overview does not cover: deleting a set asks first, Shift-click on a set's name selects a range of sets to delete together, the panel menu's Delete does the same, and Default Actions and the last set can be deleted. Undo does not bring a deleted set back. - Actions panel: the panel menu's Duplicate copies the selected action, else the current action set with all its actions ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.duplicate`). VectorCraft's own rules, which the overview does not cover: the copy goes right after the original, is named after it with " copy" added and is selected. - Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used. From f3339a47f847da8650e50e7dd17592ed7d425596 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 17:28:45 -0800 Subject: [PATCH 2/2] Actions: Insert Menu Item leaves out Image Trace and Pattern, counts recorded steps Object > Image Trace > Make and Make and Expand, and Object > Pattern > Make and Edit Pattern, are no longer listed or accepted by `ui.action.insertMenuItem`. Their menu items do more than run the command: Image Trace traces off the UI thread with progress and Cancel, and the Pattern items open the Pattern Options panel. Action playback runs only the command. Insert Stop and Insert Menu Item, given a set and action, now count the steps that the recording in progress adds to that set, so the set as Stop Playing/Recording leaves it stays within 10,000 steps per action and 8 MiB as a file. The command descriptions list every kind of menu item that cannot be a step. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/mcp/src/ui_catalog.rs | 4 +- .../src/dialogs/action_insert_menu_item.rs | 23 +++- crates/ui-egui/src/menus/entries.rs | 10 +- crates/ui-egui/src/menus/ui_commands.rs | 4 +- crates/ui-egui/src/panels/actions.rs | 101 +++++++++++++++++- docs/mcp.md | 10 +- docs/target-app-parity.md | 2 +- 7 files changed, 137 insertions(+), 17 deletions(-) diff --git a/crates/mcp/src/ui_catalog.rs b/crates/mcp/src/ui_catalog.rs index 9e4237622..dfda5f0ca 100644 --- a/crates/mcp/src/ui_catalog.rs +++ b/crates/mcp/src/ui_catalog.rs @@ -1377,13 +1377,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "ui.action.insertStop", "Insert Stop", "", - "{set?: index, action?: index, step?: index, message?, allowContinue?: bool} insert a stop, at which a playing action waits and shows message. With set and action (0 is the top set or action), the stop goes into that action, after step, else after its last step; without them it goes into the recording in progress, after the steps recorded so far. message defaults to none, and allowContinue, which offers Continue in the message box, defaults to false. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, no action and no recording, or a stop that would give the action more than 10,000 steps or its set's file more than 8 MiB is an error and changes nothing. The panel menu's Insert Stop… (dialog `actionStopRecord` with message, allowContinue, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", + "{set?: index, action?: index, step?: index, message?, allowContinue?: bool} insert a stop, at which a playing action waits and shows message. With set and action (0 is the top set or action), the stop goes into that action, after step, else after its last step; without them it goes into the recording in progress, after the steps recorded so far. message defaults to none, and allowContinue, which offers Continue in the message box, defaults to false. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, no action and no recording, or a stop that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Stop… (dialog `actionStopRecord` with message, allowContinue, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", ), ( "ui.action.insertMenuItem", "Insert Menu Item", "", - "{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", + "{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a batch (command.batch), a preference (prefs.*) or action (action.*) command, an Object › Align item, Image Trace's Make or Make and Expand, Pattern's Make or Edit Pattern, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", ), ( "ui.action.continue", diff --git a/crates/ui-egui/src/dialogs/action_insert_menu_item.rs b/crates/ui-egui/src/dialogs/action_insert_menu_item.rs index 77dafac83..2cf0ab612 100644 --- a/crates/ui-egui/src/dialogs/action_insert_menu_item.rs +++ b/crates/ui-egui/src/dialogs/action_insert_menu_item.rs @@ -208,8 +208,9 @@ mod tests { } /// The list holds the menu items that run an engine command without asking for values first, - /// outside the application, File, Window and Help menus, and leaves out Undo, Redo, the Pastes - /// and the saved-selection, saved-view and recent-font items. + /// outside the application, File, Window and Help menus, and leaves out Undo, Redo, the Pastes, + /// Object › Align, Image Trace's Make and Make and Expand, Pattern's Make and Edit Pattern and the + /// saved-selection, saved-view and recent-font items. #[test] fn the_list_holds_only_menu_items_that_can_be_steps() { let items = menus::insertable_items(true); @@ -224,9 +225,21 @@ mod tests { for command in ["object.group", "object.ungroup", "object.arrange.bringToFront"] { assert!(has(command), "{command}"); } - for command in - ["edit.undo", "edit.redo", "edit.paste", "edit.pasteInFront", "object.move", "object.align", "file.save", "view.zoomIn", "select.recall1"] - { + for command in [ + "edit.undo", + "edit.redo", + "edit.paste", + "edit.pasteInFront", + "object.move", + "object.align", + "imageTrace.make", + "imageTrace.makeAndExpand", + "object.pattern.make", + "object.pattern.edit", + "file.save", + "view.zoomIn", + "select.recall1", + ] { assert!(!has(command), "{command}"); } } diff --git a/crates/ui-egui/src/menus/entries.rs b/crates/ui-egui/src/menus/entries.rs index e2f564b1d..42ad7b655 100644 --- a/crates/ui-egui/src/menus/entries.rs +++ b/crates/ui-egui/src/menus/entries.rs @@ -144,19 +144,27 @@ pub struct InsertableItem { /// files), Window (panels and workspaces) and Help. const NOT_INSERTED_MENUS: [&str; 4] = ["VectorCraft", "File", "Window", "Help"]; +/// Commands whose menu items do more than run the command, which action playback does not do: +/// Object › Image Trace › Make and Make and Expand trace off the UI thread with progress and Cancel +/// ([`crate::trace_runs::start`]), and Object › Pattern › Make and Edit Pattern open the Pattern +/// Options panel. +const MENU_ONLY_COMMANDS: [&str; 4] = ["imageTrace.make", "imageTrace.makeAndExpand", "object.pattern.make", "object.pattern.edit"]; + /// Whether the item labeled `label` in menu `title`, which runs command `id` with `p`, can be a step /// of an action. It can when it runs an engine command, which action playback runs as the menu /// does; its label does not end with "…", the mark of an item that asks for values first; it is not /// in a menu of [`NOT_INSERTED_MENUS`]; and it is not Undo, Redo, a Paste, an item that stands for a /// saved selection, a saved view or a recent font, a plug-in's item or an action's own command. Nor /// is it Object › Align, whose menu items add the Align To choice and the selection's key to their -/// params when clicked ([`crate::panels::align::with_align_to`]), which playback does not. +/// params when clicked ([`crate::panels::align::with_align_to`]), which playback does not, or another +/// item of [`MENU_ONLY_COMMANDS`]. pub fn insertable(title: &str, label: &str, id: &str, p: &Value) -> bool { use super::slots::{RECENT_FONT_IDS, SAVED_SELECTION_IDS, SAVED_VIEW_IDS}; vectorcraft_engine::find_command(id).is_some() && !label.ends_with('…') && !NOT_INSERTED_MENUS.contains(&title) && !matches!(id, "edit.undo" | "edit.redo" | "command.batch" | "object.align") + && !MENU_ONLY_COMMANDS.contains(&id) && !id.starts_with("edit.paste") && !id.starts_with("prefs.") && !id.starts_with("action.") diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 7d80e762e..7f16be5e7 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1378,13 +1378,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ crate::panels::actions::INSERT_STOP, "Insert Stop", "", - "{set?: index, action?: index, step?: index, message?, allowContinue?: bool} insert a stop, at which a playing action waits and shows message. With set and action (0 is the top set or action), the stop goes into that action, after step, else after its last step; without them it goes into the recording in progress, after the steps recorded so far. message defaults to none, and allowContinue, which offers Continue in the message box, defaults to false. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, no action and no recording, or a stop that would give the action more than 10,000 steps or its set's file more than 8 MiB is an error and changes nothing. The panel menu's Insert Stop… (dialog `actionStopRecord` with message, allowContinue, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", + "{set?: index, action?: index, step?: index, message?, allowContinue?: bool} insert a stop, at which a playing action waits and shows message. With set and action (0 is the top set or action), the stop goes into that action, after step, else after its last step; without them it goes into the recording in progress, after the steps recorded so far. message defaults to none, and allowContinue, which offers Continue in the message box, defaults to false. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, no action and no recording, or a stop that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Stop… (dialog `actionStopRecord` with message, allowContinue, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", ), ( crate::panels::actions::INSERT_MENU_ITEM, "Insert Menu Item", "", - "{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", + "{set?: index, action?: index, step?: index, command, params?: object} insert the command of a menu item as a step, without running it; the step plays as choosing the item runs it. command and params (default {}) must be those of a menu item that can be a step: one that runs an engine command, whose label does not end with \"…\", outside the VectorCraft, File, Window and Help menus, and that is not Undo, Redo, a Paste, a batch (command.batch), a preference (prefs.*) or action (action.*) command, an Object › Align item, Image Trace's Make or Make and Expand, Pattern's Make or Edit Pattern, a saved selection, saved view or recent font item, or a plug-in's item. The place is as ui.action.insertStop takes it: with set and action, after step, else after the action's last step; without them, the recording in progress. The new step becomes the panel's selected step, and its action and set become the selected ones. A command and params that no such menu item has, a param of another type, params that are not an object, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file more than 8 MiB, counting the steps that the recording in progress adds to the set, is an error and changes nothing. The panel menu's Insert Menu Item… (dialog `actionInsertMenuItem` with find, command, params, set, action and step) runs this on OK → {set, action, step}, or {recording: true}", ), ( crate::panels::actions::CONTINUE_PLAYBACK, diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 28f53fdf2..de6b36bc6 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -937,7 +937,33 @@ fn recording_room(app: &VectorcraftApp, step: &(String, Value)) -> Result<(), St Ok(()) } -/// Puts `step` where params `p` say ([`insert_stop`]) → what [`insert_stop`] returns. +/// What Stop Playing/Recording adds to a set ([`recorded_into`]). +struct Recorded { + /// The set's action that the recording records into; None for the new action that the + /// recording adds. + into: Option, + /// The new action's name. + name: String, + /// The steps recorded so far. + steps: Vec<(String, Value)>, +} + +/// What Stop Playing/Recording adds to set `set`; None when the recording in progress adds nothing +/// to it. +fn recorded_into(app: &VectorcraftApp, set: usize) -> Option { + let (recording_set, name, start) = app.ui.recording.as_ref()?; + let into = match app.ui.recording_into { + Some(id) => Some(app.ui.action_sets.get(set)?.actions.iter().position(|a| a.id == id)?), + None if *recording_set == set => None, + None => return None, + }; + let steps = app.session.journal_for_action(app.session.journal.held().unwrap_or(*start)); + Some(Recorded { into, name: name.clone(), steps }) +} + +/// Puts `step` where params `p` say ([`insert_stop`]) → what [`insert_stop`] returns. When the +/// recording in progress adds steps to the set, the limits count them as Stop Playing/Recording +/// would leave the set. fn insert(app: &mut VectorcraftApp, p: &Value, step: (String, Value)) -> Result { let index = |k: &str| p.get(k).and_then(Value::as_u64).and_then(|i| usize::try_from(i).ok()); if p.get("set").is_none() && p.get("action").is_none() && p.get("step").is_none() { @@ -949,19 +975,35 @@ fn insert(app: &mut VectorcraftApp, p: &Value, step: (String, Value)) -> Result< return Ok(json!({"recording": true})); } let (set, at) = index("set").zip(index("action")).ok_or("no such action")?; + let recorded = recorded_into(app, set); + let pending = match &recorded { + Some(r) if r.into == Some(at) => r.steps.len(), + _ => 0, + }; let s = app.ui.action_sets.get_mut(set).filter(|s| at < s.actions.len()).ok_or("no such action")?; let action = &mut s.actions[at]; let after = match p.get("step") { None => action.steps.len(), Some(_) => index("step").filter(|&s| s < action.steps.len()).ok_or("no such action step")? + 1, }; - if action.steps.len() >= files::MAX_ACTION_STEPS { + if action.steps.len() + pending >= files::MAX_ACTION_STEPS { return Err("the action already has 10,000 steps".into()); } let excluded = action.excluded_steps.clone(); action.insert_step(after, step); let id = action.id; - if files::write_set(s).len() > files::MAX_SET_FILE { + let file_len = match recorded { + None => files::write_set(s).len(), + Some(Recorded { into, name, steps }) => { + let mut kept = s.clone(); + match into { + Some(into) => kept.actions[into].steps.extend(steps), + None => kept.actions.push(Action::new(name, steps)), + } + files::write_set(&kept).len() + } + }; + if file_len > files::MAX_SET_FILE { let action = &mut s.actions[at]; action.steps.remove(after); action.excluded_steps = excluded; @@ -4116,6 +4158,12 @@ mod tests { (place(json!({"command": "edit.paste"})), none), // Align, whose menu items add the Align To choice when clicked. (place(json!({"command": "object.align", "params": {"horizontal": "left"}})), none), + // Image Trace, whose menu items trace off the UI thread with progress and Cancel. + (place(json!({"command": "imageTrace.make", "params": {"preset": "Default"}})), none), + (place(json!({"command": "imageTrace.makeAndExpand", "params": {"preset": "Default"}})), none), + // Pattern's Make and Edit Pattern, whose menu items open the Pattern Options panel. + (place(json!({"command": "object.pattern.make"})), none), + (place(json!({"command": "object.pattern.edit"})), none), // Params that no menu item gives its command. (place(json!({"command": "type.changeCase"})), none), (place(json!({"command": "type.changeCase", "params": {"case": "middle"}})), none), @@ -4225,6 +4273,53 @@ mod tests { assert!(files::write_set(&app.ui.action_sets[0]).len() <= files::MAX_SET_FILE); } + /// A step put by its set and action into the action that the recording in progress records into + /// counts the steps recorded so far, which Stop Playing/Recording adds to that action. + #[test] + fn steps_inserted_into_the_action_being_recorded_into_count_the_recorded_steps() { + let (mut app, _) = app_with_three_moves(); + app.ui.action_sets[0].actions.push(Action::new("Other", vec![])); + let into = app.ui.action_sets[0].actions[0].id; + let stored = app.ui.action_sets[0].actions[0].steps.len(); + app.run(START_RECORDING, json!({})).unwrap(); + app.ui.recording_into = Some(into); + for _ in stored..files::MAX_ACTION_STEPS { + app.session.journal.push(("object.group".to_string(), json!({}))); + } + let steps = "the action already has 10,000 steps"; + assert_eq!(app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "command": "object.group"})).unwrap_err(), steps); + assert_eq!(app.run(INSERT_STOP, json!({"set": 0, "action": 0})).unwrap_err(), steps); + assert_eq!(app.ui.action_sets[0].actions[0].steps.len(), stored, "nothing was inserted"); + assert!(app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 1, "command": "object.group"})).is_ok(), "another action takes the step"); + app.run(STOP_RECORDING, json!({})).unwrap(); + let saved = files::write_set(&app.ui.action_sets[0]); + assert_eq!(files::read_set(&saved, "Test").unwrap().actions[0].steps.len(), files::MAX_ACTION_STEPS, "and loads again"); + + let (mut app, _) = app_with_three_moves(); + let pad = |n: usize| ("object.move".to_string(), json!({"dx": 1, "dy": 0, "pad": "x".repeat(n)})); + let into = app.ui.action_sets[0].actions[0].id; + let room = files::MAX_SET_FILE - files::write_set(&app.ui.action_sets[0]).len(); + app.run(START_RECORDING, json!({})).unwrap(); + app.ui.recording_into = Some(into); + app.session.journal.push(pad(room - 8)); + let size = "the action set would be larger than 8 MiB"; + assert_eq!(app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 0, "command": "object.group"})).unwrap_err(), size); + assert_eq!(app.run(INSERT_STOP, json!({"set": 0, "action": 0})).unwrap_err(), size); + // Another action of the set counts them too, for a recording into an action and for a + // recording that adds a new action. + for into_first in [true, false] { + let (mut app, _) = app_with_three_moves(); + app.ui.action_sets[0].actions.push(Action::new("Other", vec![])); + let first = app.ui.action_sets[0].actions[0].id; + app.run(START_RECORDING, json!({})).unwrap(); + app.ui.recording_into = into_first.then_some(first); + app.session.journal.push(pad(room - 8)); + let r = app.run(INSERT_MENU_ITEM, json!({"set": 0, "action": 1, "command": "object.group"})); + assert_eq!(r.unwrap_err(), size, "recording into the first action: {into_first}"); + assert!(app.ui.action_sets[0].actions[1].steps.is_empty(), "recording into the first action: {into_first}"); + } + } + /// A step inserted from the menu saves and loads as the command and params it holds, with its /// place and exclusion. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index 76589bbc6..06cb402d8 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -2626,18 +2626,22 @@ already walks Image Trace. Short agent entry points: VectorCraft's own rule. A stop in an action becomes the panel's selected step, and its action and set become the selected ones. An index past the last set, action or step, a param of another type, params that are not an object, no action and no recording, or a stop that would give the action more than 10,000 steps or its set's file - more than 8 MiB is an error, and then nothing changes. The panel menu's Insert Stop… opens the `actionStopRecord` dialog + more than 8 MiB is an error, and then nothing changes. The limits count the steps that the recording in progress + adds to the set. The panel menu's Insert Stop… opens the `actionStopRecord` dialog (`message`, `allowContinue`, `set`, `action`, `step`), whose OK runs this. - Actions: `ui.action.insertMenuItem {set?, action?, step?, command, params?}` inserts a menu item's command as a step without running it. `command` and `params` (default: `{}`) must be those of an item in the menus that can be a step: an item that runs a document command without opening a dialog first, outside the File, Window and Help menus, and - not Undo, Redo, a Paste command, Object › Align (its items add the Align To choice when clicked), a preference + not Undo, Redo, a Paste command, Object › Align (its items add the Align To choice when clicked), Object › Image + Trace › Make or Make and Expand (the menu traces off the UI thread, with progress and Cancel), Object › Pattern › + Make or Edit Pattern (the menu also opens the Pattern Options panel), a preference toggle, an Actions command or a recent-font, saved-selection or saved-view slot. Disabled items are listed, and playback checks the step when it runs. With `set` and `action`, the step goes into that action, after step `step`, else after its last step, becomes the panel's selected step, and the command returns `{set, action, step}`. Without them it goes into the recording in progress and returns `{recording: true}`. A command no such item runs, params that are not an object, an index past the last set, action or step, no action and no recording, or a step that would give the action more than 10,000 steps or its set's file - more than 8 MiB is an error, and then nothing changes. The panel menu's Insert Menu Item… opens the + more than 8 MiB is an error, and then nothing changes. The limits count the steps that the recording in progress + adds to the set. The panel menu's Insert Menu Item… opens the `actionInsertMenuItem` dialog (`find`, `command`, `params`, `set`, `action`, `step`): typing in Find and pressing Find picks the one item whose menu path, shown name or command id holds every word typed, or lists the matches, and OK runs this. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 8b2b77a24..e490d1c69 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -711,7 +711,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Actions panel: expand an action to show its recorded steps, then clear a step's checkmark to keep it in the action but skip it during playback. The action and set checkmarks include or exclude all their steps; a mixed checkmark includes all. Alt/Option-clicking a step checkmark keeps that step as it was and applies the opposite inclusion state to the other steps in its action. This follows the public [Exclude commands from an action](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/exclude-commands-from-actions.html) guide; the scope of the mixed-state and Alt-click behavior is VectorCraft's design. The exclusion survives UI restart, action duplication, and `.vcactions` save/load. Existing version 1 action set files load with every step included. - Actions panel: the panel menu's Playback Options… sets how actions play: Accelerated (the default) plays every step at once, Step by Step plays one step at a time with the canvas drawn after each, and Pause For waits the given number of seconds between steps ([Specify playback speed](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/specify-playback-speed.html); `ui.action.playbackOptions`). VectorCraft's own rules, which the page does not cover: the pause is a whole number of seconds from 1 to 60, the setting is kept with the UI preferences, a paced playback ends at Stop Playing/Recording or when a step comes due while another document is active, each step of a paced playback is its own undo step, and Batch… plays at full speed. - Actions panel: the panel menu's Insert Stop… opens Record Stop, with a Message box and Allow Continue, and adds a stop to the recording in progress, after the selected command, or at the end of the selected action ([Insert stops in actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-stops-in-actions.html); `ui.action.insertStop`). A playing action waits at the stop and shows the message so the user can do a manual task, and Play Current Selection plays the rest ([Illustrator CS5 help](https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=459); `ui.action.continue`). With Allow Continue, the message box also has Continue. VectorCraft's own rules, which the pages do not cover: an empty message still waits, Stop Playing/Recording ends the playback and keeps the steps already played, the rest plays on the document active then, a stop waits at every playback speed, at Accelerated the steps between two stops are one undo step, Batch… refuses an action with an included stop, and Delete Selection on a selected step deletes that step, a stop or a command, without asking (`ui.action.deleteStep`). - - Actions panel: the panel menu's Insert Menu Item… adds a menu command to the recording in progress, after the selected step, or at the end of the selected action, without running it, and playback runs it as the menu does ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertMenuItem`). The dialog's Find box picks the one item that matches the words typed, and a click on a listed item picks it. VectorCraft's own rules, which the page does not cover: items that open a dialog, the Object › Align items and the File, Window and Help menu items are not listed, and an action holds at most 10,000 steps and its set file at most 8 MiB, for stops too. + - Actions panel: the panel menu's Insert Menu Item… adds a menu command to the recording in progress, after the selected step, or at the end of the selected action, without running it, and playback runs it as the menu does ([Insert non-recordable tasks into actions](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/insert-non-recordable-tasks-into-actions.html); `ui.action.insertMenuItem`). The dialog's Find box picks the one item that matches the words typed, and a click on a listed item picks it. VectorCraft's own rules, which the page does not cover: items that open a dialog, the Object › Align items, Image Trace's Make and Make and Expand, Pattern's Make and Edit Pattern, and the File, Window and Help menu items are not listed, and an action holds at most 10,000 steps and its set file at most 8 MiB, for stops too. - Actions panel: Delete Selection deletes the selected action set with its actions and keeps the other sets ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.delete`). VectorCraft's own rules, which the overview does not cover: deleting a set asks first, Shift-click on a set's name selects a range of sets to delete together, the panel menu's Delete does the same, and Default Actions and the last set can be deleted. Undo does not bring a deleted set back. - Actions panel: the panel menu's Duplicate copies the selected action, else the current action set with all its actions ([Actions panel overview](https://helpx.adobe.com/illustrator/desktop/automate-visualize-data/automate-actions/actions-panel-overview.html); `ui.actionSet.duplicate`). VectorCraft's own rules, which the overview does not cover: the copy goes right after the original, is named after it with " copy" added and is selected. - Batch… over data sets names each file after the document and the data set (`action.batch`). It writes `/`, `\`, `:`, `<`, `>`, `"`, `|`, `?`, `*` and control characters in the data set's name as `-`, and adds `-2`, `-3`… to a name an earlier data set used.