diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 8e29bccb4..b283472f3 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -320,6 +320,13 @@ pub fn handle(app: &mut SheetApp, ctx: &egui::Context, method: &str, p: &Json) - None => err("no dialog is open"), }, "ui.dialog.cancel" => { + // The macro step editor's draft, overlays and debug run go with its own close. + if app.dialog.as_ref().is_some_and(|d| d.name == "macroEditor") { + return match crate::macro_editor::close_now(app) { + Ok(()) => ok(Json::Null), + Err(e) => err(e), + }; + } app.dialog = None; ok(Json::Null) } diff --git a/crates/ui-egui/src/macro_editor.rs b/crates/ui-egui/src/macro_editor.rs index f3e934c6a..51450470f 100644 --- a/crates/ui-egui/src/macro_editor.rs +++ b/crates/ui-egui/src/macro_editor.rs @@ -838,6 +838,26 @@ pub fn window(app: &mut SheetApp, ctx: &egui::Context, d: &mut Dialog, open: &mu } } +/// Closes the step editor at once for a caller outside its window (the control channel's +/// `ui.dialog.cancel`), with the cleanup its own close does. Refused while it holds a debug run +/// or unsaved steps, which only its window asks about. +pub fn close_now(app: &mut SheetApp) -> Result<(), String> { + if app.session.macro_debug.is_some() { + return Err("A macro is being debugged: stop it first (macro.debug {\"action\": \"stop\"}).".into()); + } + if app.macros.editor.as_ref().is_some_and(|ed| ed.dirty) { + return Err("The macro step editor has unsaved steps: save them or close the editor in its window.".into()); + } + if let Some(ed) = app.macros.editor.take() + && app.session.active().is_some_and(|d| d.uid == ed.doc) + { + restore_home(app, &ed.refs); + } + app.grid.macro_overlay = None; + app.dialog = None; + Ok(()) +} + // ---------------------------------------------------------------- reference boxes /// A range as an absolute reference: `$B$2:$D$9`. diff --git a/crates/ui-egui/tests/it/macro_editor.rs b/crates/ui-egui/tests/it/macro_editor.rs index 0e986da75..169e4d480 100644 --- a/crates/ui-egui/tests/it/macro_editor.rs +++ b/crates/ui-egui/tests/it/macro_editor.rs @@ -402,3 +402,30 @@ fn the_editor_works_only_on_the_workbook_it_opened_in() { assert!(h.state().macros.editor.is_none()); assert!(h.state().dialog.as_ref().is_none_or(|d| d.name() != "macroEditor")); } + +#[test] +fn the_control_channels_cancel_closes_the_editor_as_its_own_close_does() { + let mut h = harness(); + three(&mut h); + let ctx = h.ctx.clone(); + let reply = |o: gridcraft_ui_egui::control::Outcome| match o { + gridcraft_ui_egui::control::Outcome::Done(v) => v, + _ => json!(null), + }; + // Unsaved steps are only the editor's to ask about: cancel is refused and keeps them. + h.state_mut().macros.editor.as_mut().unwrap().dirty = true; + let r = reply(gridcraft_ui_egui::control::handle(h.state_mut(), &ctx, "ui.dialog.cancel", &json!({}))); + assert!(r.get("error").is_some(), "{r}"); + assert!(h.state().macros.editor.is_some()); + // A saved editor closes, and takes its overlays with it. + h.state_mut().macros.editor.as_mut().unwrap().dirty = false; + h.get_by_label("Check").click(); + h.run_steps(3); + assert!(h.state().grid.macro_overlay.is_some()); + let r = reply(gridcraft_ui_egui::control::handle(h.state_mut(), &ctx, "ui.dialog.cancel", &json!({}))); + assert!(r.get("error").is_none(), "{r}"); + h.run_steps(2); + assert!(h.state().dialog.is_none()); + assert!(h.state().macros.editor.is_none()); + assert!(h.state().grid.macro_overlay.is_none()); +}