Merge pull request #6731 from dotCooCoo/feat/actions-insert-menu-item

Actions: Insert Menu Item adds a menu command to an action
This commit is contained in:
BFlat
2026-11-12 09:40:12 +07:00
committed by GitHub
38 changed files with 1096 additions and 17 deletions
+7 -1
View File
@@ -1395,7 +1395,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, 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 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",
+2
View File
@@ -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;
@@ -391,6 +392,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,
@@ -0,0 +1,261 @@
//! 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<usize>) {
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<Row> {
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::<Vec<_>>().join(" › ");
let shown = parts().map(|s| crate::i18n::tr(lang, s)).collect::<Vec<_>>().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<Vec<Row>> {
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<Vec<Row>>)>(id))
&& k == key
{
return v;
}
let built = Arc::new(build(lang, english_names));
ctx.data_mut(|d| d.insert_temp::<(Key, Arc<Vec<Row>>)>(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<Item = &'a Row> {
let words: Vec<String> = 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<usize> = 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<Value, String> {
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,
/// 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);
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",
"imageTrace.make",
"imageTrace.makeAndExpand",
"object.pattern.make",
"object.pattern.edit",
"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");
}
}
+10
View File
@@ -5889,6 +5889,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 Стъпка по стъпка
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5928,6 +5928,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -3942,6 +3942,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6118,6 +6118,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5904,6 +5904,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5701,6 +5701,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6069,6 +6069,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 Βήμα προς βήμα
Can't render this file because it is too large.
+10
View File
@@ -6060,6 +6060,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6148,6 +6148,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6113,6 +6113,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -3391,6 +3391,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 प्लगइन जानकारी
Can't render this file because it is too large.
+10
View File
@@ -6101,6 +6101,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5904,6 +5904,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
Can't render this file because it contains an unexpected character in line 2338 and column 5.
+10
View File
@@ -5735,6 +5735,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5800,6 +5800,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 ステップ実行
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6069,6 +6069,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 ნაბიჯ-ნაბიჯ
Can't render this file because it is too large.
+10
View File
@@ -5696,6 +5696,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 단계별
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5955,6 +5955,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5972,6 +5972,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
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -6033,6 +6033,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
Can't render this file because it contains an unexpected character in line 6 and column 47.
+10
View File
@@ -5729,6 +5729,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 Пошагово
Can't render this file because it is too large.
+10
View File
@@ -6137,6 +6137,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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+10
View File
@@ -6074,6 +6074,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
Can't render this file because it contains an unexpected character in line 5 and column 25.
+10
View File
@@ -5715,6 +5715,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 Покроково
Can't render this file because it contains an unexpected character in line 182 and column 5.
+10
View File
@@ -5716,6 +5716,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
Can't render this file because it contains an unexpected character in line 204 and column 5.
+10
View File
@@ -5956,6 +5956,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 逐步
Can't render this file because it contains an unexpected character in line 2 and column 73.
+10
View File
@@ -5753,6 +5753,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 逐步
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -1
View File
@@ -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;
+73
View File
@@ -127,6 +127,79 @@ pub fn context_entries(app: &VectorcraftApp) -> Vec<MenuEntry> {
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"];
/// 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, 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.")
&& ![&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<InsertableItem> {
fn walk(title: &'static str, path: &mut Vec<&'static str>, items: &[Item], out: &mut Vec<InsertableItem>) {
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<String>, items: &[Item], out: &mut Vec<MenuEntry>) {
for it in items {
match it {
+1
View File
@@ -919,6 +919,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result
crate::panels::actions::START_RECORDING => 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" => {
+7 -1
View File
@@ -1396,7 +1396,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, 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 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,
+451 -12
View File
@@ -870,31 +870,145 @@ 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<Value, String> {
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<Value, String> {
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(())
}
/// 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<usize>,
/// 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<Recorded> {
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<Value, String> {
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 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() + 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;
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;
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 +1036,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<Value, String> {
@@ -943,7 +1057,7 @@ pub fn delete_step(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
.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 +1304,16 @@ pub fn delete_sets(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String>
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 +1360,9 @@ pub fn duplicate(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
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 +1437,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 +4087,253 @@ 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),
// 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),
(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 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]
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 +4375,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 +4861,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<String> = 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::<Option<(u64, usize)>>(&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();
+1 -1
View File
@@ -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;
+19 -1
View File
@@ -2739,8 +2739,26 @@ 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 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), 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 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.
- 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
+1
View File
@@ -714,6 +714,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, 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.