From b5c69e25fdd9113f9d9b139973faa93966f3c3a2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 18:25:02 -0800 Subject: [PATCH] Add command to begin recording Actions --- crates/ui-egui/src/menus/run.rs | 1 + crates/ui-egui/src/menus/ui_commands.rs | 6 ++ crates/ui-egui/src/panels/actions.rs | 80 ++++++++++++++++++++----- docs/mcp.md | 4 ++ 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 5f3969546..1666e147e 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -823,6 +823,7 @@ fn run_ui_part11(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::panels::actions::new_action(app, p), crate::panels::actions::ACTION_OPTIONS => crate::panels::actions::action_options(app, p), crate::panels::actions::PLAY_ACTION => crate::panels::actions::play_action(app, p), + crate::panels::actions::START_RECORDING => crate::panels::actions::record(app, p), crate::panels::actions::STOP_RECORDING => crate::panels::actions::stop(app, p), "ui.batchDialog" => { let set = p.get("set").and_then(Value::as_u64).unwrap_or(0) as usize; diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 31b263d3b..d4677597a 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1205,6 +1205,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{action: name, set?: name} play the Actions panel's action named action as one undoable batch; set names the set to look in (default: every set). A name no action has, or one that more than one action in the sets looked in has, is an error and nothing plays → what command.batch returns", ), + ( + crate::panels::actions::START_RECORDING, + "Begin Recording", + "", + "{set?: index} begin recording an automatically named Action N in the Actions panel's set at set (0 is the top set), by default the current set or the first set; with no sets, make Set 1. The set becomes current, and ui.action.stop adds the recorded action after that set's existing actions. An explicit invalid set or an existing recording is an error and changes nothing → {set, name}", + ), ( crate::panels::actions::STOP_RECORDING, "Stop Playing/Recording", diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index 174fab9bc..8948fbef2 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -235,8 +235,10 @@ pub fn new_action(app: &mut VectorcraftApp, p: &Value) -> Result s.actions.push(action); let index = s.actions.len() - 1; start_recording(app, set, name, Some(id)); - app.ui.action_set = Some(set); - app.ui.action_sets_selected = vec![set]; + if app.ui.action_set != Some(set) { + app.ui.action_set = Some(set); + app.ui.action_sets_selected = vec![set]; + } if let Some(ctx) = app.ui_ctx.clone() { set_pstate::>(&ctx, "actions-selected", Some((set, index))); } @@ -328,6 +330,40 @@ pub fn play_action(app: &mut VectorcraftApp, p: &Value) -> Result /// The command that ends a recording ([`stop`]). pub const STOP_RECORDING: &str = "ui.action.stop"; +/// The command that begins a plain recording ([`record`]). +pub const START_RECORDING: &str = "ui.action.record"; + +/// `ui.action.record {set?}`: begins recording an automatically named Action N in `set`, or in +/// the current set (the first if none is current). With no sets, makes Set 1. Stop adds the action +/// after the set's existing actions. An explicit invalid set or an existing recording is an error +/// and leaves the panel unchanged. → {set, name} +pub fn record(app: &mut VectorcraftApp, p: &Value) -> Result { + if app.ui.recording.is_some() { + return Err("an action is already recording".into()); + } + let set = match p.get("set") { + Some(v) => v.as_u64().and_then(|i| usize::try_from(i).ok()).filter(|&i| i < app.ui.action_sets.len()).ok_or("no such action set")?, + _ => { + if app.ui.action_sets.is_empty() { + app.ui.action_sets.push(ActionSet::new("Set 1", vec![])); + } + app.ui.action_set.filter(|&i| i < app.ui.action_sets.len()).unwrap_or(0) + } + }; + app.ui.action_set = Some(set); + app.ui.action_sets_selected = vec![set]; + let mut number = app.ui.action_sets[set].actions.len() + 1; + let name = loop { + let name = format!("Action {number}"); + if app.ui.action_sets[set].actions.iter().all(|action| action.name != name) { + break name; + } + number += 1; + }; + start_recording(app, set, name.clone(), None); + Ok(json!({ "set": set, "name": name })) +} + /// `ui.action.stop {}`: ends the recording in progress and keeps its steps, in the action New /// Action made for it, else in a new action after the last action of its set, as the bottom bar's /// Stop Playing/Recording does. With nothing recording it changes nothing. → {stopped} @@ -786,19 +822,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { } let (r, resp) = ui.allocate_exact_size(egui::vec2(24.0, 24.0), egui::Sense::click()); ui.painter().circle_filled(r.center(), 6.0, if recording { egui::Color32::from_rgb(0xe0, 0x30, 0x30) } else { t.icon }); - if resp.on_hover_text(tl!("Begin Recording")).clicked() && !recording { - if app.ui.action_sets.is_empty() { - app.ui.action_sets.push(ActionSet::new("Set 1", vec![])); - } - let set = current.filter(|&s| s < app.ui.action_sets.len()).unwrap_or(0); - // With no set current, the recording goes into the first set, which becomes current so - // the panel shows where the recording goes. - if app.ui.action_set != Some(set) { - app.ui.action_set = Some(set); - app.ui.action_sets_selected = vec![set]; - } - let n = app.ui.action_sets.get(set).map_or(0, |s| s.actions.len()) + 1; - start_recording(app, set, format!("Action {n}"), None); + if resp.on_hover_text(tl!("Begin Recording")).clicked() + && !recording + && let Err(e) = app.run(START_RECORDING, json!({})) + { + app.status(e); } if widgets::icon_button(ui, "dc-actions", tl!("Play Current Selection"), false, 24.0).clicked() && let Some(s) = selected @@ -847,6 +875,28 @@ mod tests { } } + #[test] + fn begin_recording_command_matches_the_panel_button() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100})).unwrap(); + app.ui.action_sets.clear(); + app.ui.action_set = None; + assert_eq!(app.run(START_RECORDING, json!({})).unwrap(), json!({"set": 0, "name": "Action 1"})); + assert_eq!((app.ui.action_sets[0].name.as_str(), app.ui.action_set), ("Set 1", Some(0))); + assert_eq!(app.run(START_RECORDING, json!({})).unwrap_err(), "an action is already recording"); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + assert_eq!(app.run(STOP_RECORDING, json!({})).unwrap(), json!({"stopped": true})); + assert_eq!((app.ui.action_sets[0].actions[0].name.as_str(), app.ui.action_sets[0].actions[0].steps.len()), ("Action 1", 1)); + + app.ui.action_sets.push(ActionSet::new("Other", vec![])); + assert_eq!(app.run(START_RECORDING, json!({"set": 99})).unwrap_err(), "no such action set"); + assert_eq!(app.run(START_RECORDING, json!({"set": null})).unwrap_err(), "no such action set"); + assert!(app.ui.recording.is_none()); + assert_eq!(app.run(START_RECORDING, json!({"set": 1})).unwrap(), json!({"set": 1, "name": "Action 1"})); + assert_eq!((app.ui.action_set, app.ui.action_sets_selected.as_slice()), (Some(1), &[1][..])); + assert_eq!(app.run(STOP_RECORDING, json!({})).unwrap(), json!({"stopped": true})); + } + /// #1629: a clicked action stays selected, so Play Current Selection plays it and Delete /// Selection deletes it. #[test] diff --git a/docs/mcp.md b/docs/mcp.md index fd739b406..af2adefa7 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1774,6 +1774,10 @@ already walks Image Trace. Short agent entry points: cannot be used. While an action is recording, and for an index past the last set, an empty name, or a key or color that is not one of these, it is an error and nothing is added. The panel's Create New Action button and menu's New Action… open the `actionNew` dialog, whose Record runs this. +- Actions: `ui.action.record {set?}` starts recording a new action in the chosen set, or the current set when `set` + is left out, and returns `{set, name}`. With no sets, it creates Set 1. The action gets the next available + `Action N` name in that set. The Actions panel's Begin Recording button runs this command. An invalid set or an + existing recording is an error and leaves the sets and recording unchanged. - Actions: `ui.action.options {set, action, name?, key?, cmd?, shift?, color?}` changes the action at `action` in set `set` and keeps its steps: `name` renames it, `key` with `cmd` and `shift` sets its function key, and `color` its Button Mode color, as `ui.action.new` takes them; a param left out keeps what the action has, and