From 60daa85f5bbce949d23c917ec4cd7af78f47e66f Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 06:15:31 -0800 Subject: [PATCH] Actions: a View menu step in a recorded batch counts in action set files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Save Actions… writes version 6 when a View menu step sits inside a `command.batch` step, since playback runs that step on its own, and Load Actions… refuses such a nested step whose params are not an object. --- crates/ui-egui/src/panels/actions.rs | 12 +++++++++--- crates/ui-egui/src/panels/actions/files.rs | 22 +++++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index a357e2cc3a..81e3074394 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -4655,7 +4655,8 @@ mod tests { } /// An action set with a View menu step saves as version 6 and loads back with the step; one whose - /// View menu step has params that are not an object is not an action set file. + /// View menu step has params that are not an object is not an action set file. A View menu step + /// in a recorded batch counts too, since playback runs it as its own step. #[test] fn a_saved_view_step_survives_as_version_6() { let (app, _) = app_with_a_view_step(); @@ -4666,9 +4667,14 @@ mod tests { assert_eq!(loaded.actions[0].steps, app.ui.action_sets[0].actions[0].steps); for params in [json!(5), json!([1]), json!(null)] { let step = json!({"command": "view.zoomIn", "params": params}); - let bad = json!({"format": "vcactions", "version": 6, "name": "Bad", "actions": [{"name": "A", "steps": [step]}]}); - assert_eq!(files::read_set(&bad.to_string(), "Test").unwrap_err(), "not an action set file", "{params}"); + for step in [step.clone(), json!({"command": "command.batch", "params": {"commands": [step]}})] { + let bad = json!({"format": "vcactions", "version": 6, "name": "Bad", "actions": [{"name": "A", "steps": [step]}]}); + assert_eq!(files::read_set(&bad.to_string(), "Test").unwrap_err(), "not an action set file", "{step}"); + } } + let nested = json!({"commands": [{"command": "view.outline", "params": {}}]}); + let set = ActionSet::new("Nested", vec![Action::new("A", vec![("command.batch".into(), nested)])]); + assert_eq!(serde_json::from_str::(&files::write_set(&set)).unwrap()["version"], json!(6), "a step in a recorded batch counts"); } /// At Accelerated, the steps up to a stop play as saved steps, as they do without a stop: a key diff --git a/crates/ui-egui/src/panels/actions/files.rs b/crates/ui-egui/src/panels/actions/files.rs index 911e6ae41b..a8ae79e713 100644 --- a/crates/ui-egui/src/panels/actions/files.rs +++ b/crates/ui-egui/src/panels/actions/files.rs @@ -153,6 +153,22 @@ fn select_paths_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> { steps_in(super::SELECT_PATH_STEP, command, params) } +/// The params of the View menu steps ([`crate::menus::plays_in_app`]) in a step, as [`stops_in`] +/// finds stops. +fn ui_steps_in<'a>(command: &str, params: &'a Value) -> Vec<&'a Value> { + if crate::menus::plays_in_app(command) { + return vec![params]; + } + if command != "command.batch" { + return vec![]; + } + let steps = params.get("commands").and_then(Value::as_array).into_iter().flatten(); + steps + .filter(|step| step.get("command").and_then(Value::as_str).is_some_and(crate::menus::plays_in_app)) + .map(|step| step.get("params").unwrap_or(&Value::Null)) + .collect() +} + /// `set` as the text of an action set file. pub fn write_set(set: &ActionSet) -> String { let actions = set @@ -174,7 +190,7 @@ pub fn write_set(set: &ActionSet) -> String { let has_select_object = steps().any(|(command, params)| !select_objects_in(command, params).is_empty()); let has_select_path = steps().any(|(command, params)| !select_paths_in(command, params).is_empty()); let has_stop = steps().any(|(command, params)| !stops_in(command, params).is_empty()); - let has_ui_step = steps().any(|(command, _)| crate::menus::plays_in_app(command)); + let has_ui_step = steps().any(|(command, params)| !ui_steps_in(command, params).is_empty()); let version = match (has_select_path, has_select_object, has_stop, set.actions.iter().any(|a| !a.excluded_steps.is_empty())) { _ if has_ui_step => 6, (true, ..) => 5, @@ -217,8 +233,8 @@ pub fn read_set(text: &str, name: &str) -> Result { if select_objects.any(|p| super::select_object_params(p).is_err()) { return Err("not an action set file".into()); } - let mut ui_steps = f.actions.iter().flat_map(|a| &a.steps).filter(|s| crate::menus::plays_in_app(&s.command)); - if ui_steps.any(|s| !s.params.is_object()) { + let mut ui_steps = f.actions.iter().flat_map(|a| &a.steps).flat_map(|s| ui_steps_in(&s.command, &s.params)); + if ui_steps.any(|p| !p.is_object()) { return Err("not an action set file".into()); } let actions = f