UI: the macro step editor works only on the workbook it opened in

The macro.* commands act on the active workbook, and the editor stays
open while another one becomes active (File > New, Switch Window, an
agent). A new draft saved then went into that other workbook, and Check,
Run and Debug ran there. The editor now keeps the workbook it opened in:
while another one is active it says so, its controls are disabled,
Save, Check, Run and starting Debug refuse with a message that names
the workbook, the grid overlays are hidden and closing does not put the
selection back on the wrong sheet. Closing the workbook closes the
editor.
This commit is contained in:
prxnvt
2026-10-25 06:48:57 -04:00
parent 73b0edd085
commit a206178209
3 changed files with 97 additions and 11 deletions
+50 -10
View File
@@ -52,6 +52,8 @@ pub struct Picker {
/// The editor's state while it is open.
#[derive(Clone, Debug)]
pub struct Editor {
/// The workbook (`DocState::uid`) the macro is in: the editor saves, checks and runs there only.
pub doc: u64,
pub draft: Macro,
/// The name the macro is saved under (`None` until it is first saved).
pub saved_as: Option<String>,
@@ -485,7 +487,9 @@ pub fn open(app: &mut SheetApp, params: &Json) -> Option<Dialog> {
let start_debug = params["debug"].as_bool() == Some(true) && saved_as.is_some();
let title = format!("Macro: {}", draft.name);
let selected = (!draft.steps.is_empty()).then(|| "1".to_string());
let doc = doc.uid;
app.macros.editor = Some(Editor {
doc,
draft,
dirty: saved_as.is_none(),
saved_as,
@@ -511,8 +515,27 @@ pub fn open(app: &mut SheetApp, params: &Json) -> Option<Dialog> {
// ---------------------------------------------------------------- actions
/// Whether the editor's workbook is the active one, which the `macro.*` commands act on. When
/// another workbook is active (File › New, Switch Window, an agent), says so and returns false.
fn at_home(app: &mut SheetApp, ed: &Editor, title: &str) -> bool {
if app.session.active().is_some_and(|d| d.uid == ed.doc) {
return true;
}
let book = home_title(app, ed);
app.message = Some((title.into(), format!("The macro “{}” is in “{book}”. Switch back to it to save, check or run the macro.", ed.draft.name)));
false
}
/// The title of the editor's workbook.
fn home_title(app: &SheetApp, ed: &Editor) -> String {
app.session.documents().iter().find(|d| d.uid == ed.doc).map(|d| d.display_title()).unwrap_or_default()
}
/// Saves the draft (renaming the saved macro if its name changed). False when it couldn't.
pub fn save(app: &mut SheetApp, ed: &mut Editor) -> bool {
if !at_home(app, ed, "Save Macro") {
return false;
}
ed.draft.name = ed.draft.name.trim().to_string();
if let Err(e) = ed.draft.check_limits() {
app.message = Some(("Save Macro".into(), e));
@@ -568,7 +591,7 @@ fn error_step(msg: &str) -> Option<(String, String)> {
pub fn check(app: &mut SheetApp, ed: &mut Editor) {
// A reference box picking from the grid would take the selections the action makes.
ed.refs.pick = None;
if !ensure_saved(app, ed) {
if !at_home(app, ed, "Check Macro") || !ensure_saved(app, ed) {
return;
}
ed.errors.clear();
@@ -619,7 +642,7 @@ fn check_text(v: &Json) -> String {
/// Run: runs the saved macro as one undo step.
pub fn run(app: &mut SheetApp, ed: &mut Editor) {
ed.refs.pick = None;
if !ensure_saved(app, ed) {
if !at_home(app, ed, "Run Macro") || !ensure_saved(app, ed) {
return;
}
ed.errors.clear();
@@ -660,7 +683,7 @@ pub fn debug_action(app: &mut SheetApp, ed: &mut Editor, action: &str) {
// Starting selects the first step: take Run to Here's target before that.
let target = ed.selected.clone().filter(|s| split_path(s).is_some());
if !debugging {
if !ensure_saved(app, ed) {
if !at_home(app, ed, "Debug Macro") || !ensure_saved(app, ed) {
return;
}
ed.errors.clear();
@@ -752,6 +775,13 @@ pub fn window(app: &mut SheetApp, ctx: &egui::Context, d: &mut Dialog, open: &mu
if ed.debug.is_some() && app.session.macro_debug.is_none() {
ed.debug = None;
}
// Its workbook was closed: the macro went with it.
if !app.session.documents().iter().any(|d| d.uid == ed.doc) {
*open = false;
return;
}
let home = app.session.active().is_some_and(|d| d.uid == ed.doc);
let home_title = home_title(app, &ed);
let dirty_mark = if ed.dirty { " •" } else { "" };
let screen = ctx.content_rect();
let mut win_open = true;
@@ -766,12 +796,21 @@ pub fn window(app: &mut SheetApp, ctx: &egui::Context, d: &mut Dialog, open: &mu
.default_pos(pos2((screen.right() - 860.0).max(screen.left() + 20.0), screen.top() + 190.0))
.open(&mut win_open)
.show(ctx, |ui| {
if let Some(key) = ed.refs.collapsed.clone() {
ref_edit(app, ui, d, &mut ed.refs, &key, 340.0);
apply_ref_value(&mut ed, d, &key);
return;
if !home {
let t = Tokens::get(ui.ctx());
ui.label(
egui::RichText::new(format!("This macro is in “{home_title}”. Switch back to it to edit, check or run the macro."))
.color(t.danger),
);
}
body(app, ui, d, &mut ed);
ui.add_enabled_ui(home, |ui| {
if let Some(key) = ed.refs.collapsed.clone() {
ref_edit(app, ui, d, &mut ed.refs, &key, 340.0);
apply_ref_value(&mut ed, d, &key);
return;
}
body(app, ui, d, &mut ed);
});
});
if ed.refs.pick.is_some() && !ed.refs.drawn {
ed.refs.pick = None;
@@ -790,10 +829,11 @@ pub fn window(app: &mut SheetApp, ctx: &egui::Context, d: &mut Dialog, open: &mu
ed.picker = None;
}
prompts(app, ctx, &mut ed, open);
app.grid.macro_overlay = if *open { overlay(app, &ed) } else { None };
// The overlays mark the editor's workbook's cells, so they show only while it is active.
app.grid.macro_overlay = if *open && home { overlay(app, &ed) } else { None };
if *open {
app.macros.editor = Some(ed);
} else {
} else if home {
restore_home(app, &ed.refs);
}
}
+43
View File
@@ -359,3 +359,46 @@ fn enter_in_change_never_picks_a_control_step() {
assert!(matches!(&ed.draft.steps[0].kind, StepKind::Command { .. }), "{search:?}: {:?}", ed.draft.steps[0].kind);
}
}
#[test]
fn the_editor_works_only_on_the_workbook_it_opened_in() {
let mut h = harness();
// A new macro whose one step writes A1, not saved yet.
h.state_mut().open_dialog("macroEditor", json!({"new": "Fresh"}));
h.run_steps(3);
let step = gridcraft_engine::model::Step::command("cell.set", json!({"cell": "A1", "input": "ran"}));
h.state_mut().macros.editor.as_mut().unwrap().draft.steps = vec![step];
let book1 = h.state().session.active().unwrap().uid;
// Another workbook becomes active while the editor is open (File › New, Switch Window, an agent).
h.state_mut().session.new_workbook();
h.run_steps(3);
let mut ed = h.state_mut().macros.editor.take().unwrap();
let app = h.state_mut();
// Save, Check, Run and Debug never act on the other workbook.
assert!(!gridcraft_ui_egui::macro_editor::save(app, &mut ed));
gridcraft_ui_egui::macro_editor::check(app, &mut ed);
gridcraft_ui_egui::macro_editor::run(app, &mut ed);
gridcraft_ui_egui::macro_editor::debug_action(app, &mut ed, "step");
assert!(app.session.macro_debug.is_none());
assert!(app.session.active().unwrap().wb.macros.is_empty(), "nothing was saved into the other workbook");
assert_eq!(value(&h, "A1"), Value::Empty, "nothing ran there");
assert!(h.state().message.as_ref().is_some_and(|(_, m)| m.contains("Book1")), "{:?}", h.state().message);
h.state_mut().message = None;
h.state_mut().macros.editor = Some(ed);
h.run_steps(3);
assert!(h.query_by_label_contains("Switch back to it").is_some(), "the editor says whose macro it is");
// Back in its own workbook, the editor saves there.
let mut ed = h.state_mut().macros.editor.take().unwrap();
let app = h.state_mut();
let i = app.session.documents().iter().position(|d| d.uid == book1).unwrap();
app.session.set_active(i);
assert!(gridcraft_ui_egui::macro_editor::save(app, &mut ed));
assert_eq!(app.session.active().unwrap().wb.macros.iter().map(|m| m.name.as_str()).collect::<Vec<_>>(), ["Fresh"]);
app.macros.editor = Some(ed);
h.run_steps(3);
// Closing the workbook (without saving it) closes its editor.
h.state_mut().run("file.close", json!({"force": true})).unwrap();
h.run_steps(3);
assert!(h.state().macros.editor.is_none());
assert!(h.state().dialog.as_ref().is_none_or(|d| d.name() != "macroEditor"));
}
+4 -1
View File
@@ -290,7 +290,10 @@ other macros open; a macro that was left out is not in the saved file.
reference boxes that take a typed reference or a selection on the grid and switch between a
token relative to the example input (Rel) and an absolute reference (Abs); the Inputs tab sets
each input's shape, header and column types, and Use current selection as example infers them;
the Output tab sets the anchor and what to do when results would land on data.
the Output tab sets the anchor and what to do when results would land on data. The editor
belongs to the workbook it opened in: while another workbook is active it says so and Save,
Check, Run and Debug wait until that workbook is active again, and closing the workbook closes
the editor.
- **Debug bar.** Check dry-runs the macro and colours the grid: inputs blue, the cells it writes
green, the cells among them that hold data red. Run runs it as one undo step. Step, Continue and
Run to Here start the debugger when needed (`macro.debug` with the editor's breakpoints and