UI: closing the window asks about the macro step editor first

A window close (the close button, Alt+F4, quitting) let the app go when
every workbook was saved, losing the editor's unsaved steps, and with an
unsaved workbook it made that workbook active under the editor and asked
for a Save Changes question the editor's window would not let open, so
nothing happened. The close now waits for the editor: it asks about its
debug run and unsaved steps as its own close button does, Cancel keeps
the app open, and once the editor has closed the window close goes on
and asks about unsaved workbooks as before.
This commit is contained in:
prxnvt
2026-10-25 06:48:57 -04:00
parent 12072c5529
commit f73e7b7e19
4 changed files with 100 additions and 5 deletions
+7
View File
@@ -1144,6 +1144,13 @@ impl SheetApp {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
return;
}
// The macro step editor asks about its own unsaved steps and debug run first; once it has
// closed, the window close goes on.
if self.dialog.as_ref().is_some_and(|d| d.name == "macroEditor") {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
macro_editor::request_close(self, true);
return;
}
if !self.session.documents().iter().any(|d| d.is_dirty()) {
return;
}
+29 -4
View File
@@ -75,6 +75,10 @@ pub struct Editor {
/// Asking whether to keep what a stopped debug run did (and then close, if `closing`).
pub ask_stop: bool,
pub closing: bool,
/// Close as the title bar's close button does (asking first) on the next frame.
pub close_requested: bool,
/// The window is quitting: once the editor has closed, the window close goes on.
pub quit: bool,
/// Asking whether to save before closing.
pub ask_close: bool,
/// Start debugging on the first frame (Macros › Step Into).
@@ -503,6 +507,8 @@ pub fn open(app: &mut SheetApp, params: &Json) -> Option<Dialog> {
errors: BTreeMap::new(),
ask_stop: false,
closing: false,
close_requested: false,
quit: false,
ask_close: false,
start_debug,
start_check: params["check"].as_bool() == Some(true),
@@ -815,7 +821,7 @@ pub fn window(app: &mut SheetApp, ctx: &egui::Context, d: &mut Dialog, open: &mu
if ed.refs.pick.is_some() && !ed.refs.drawn {
ed.refs.pick = None;
}
if !win_open {
if !win_open || std::mem::take(&mut ed.close_requested) {
if app.session.macro_debug.is_some() {
ed.ask_stop = true;
ed.closing = true;
@@ -833,8 +839,23 @@ pub fn window(app: &mut SheetApp, ctx: &egui::Context, d: &mut Dialog, open: &mu
app.grid.macro_overlay = if *open && home { overlay(app, &ed) } else { None };
if *open {
app.macros.editor = Some(ed);
} else if home {
restore_home(app, &ed.refs);
} else {
if home {
restore_home(app, &ed.refs);
}
if ed.quit {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
}
}
/// Asks the open step editor to close, as its close button does: it asks about a debug run or
/// unsaved steps first. With `quit` (the window is closing), the window close goes on once the
/// editor has closed; a Cancel in its questions keeps both open.
pub fn request_close(app: &mut SheetApp, quit: bool) {
if let Some(ed) = app.macros.editor.as_mut() {
ed.close_requested = true;
ed.quit = quit;
}
}
@@ -966,6 +987,7 @@ fn prompts(app: &mut SheetApp, ctx: &egui::Context, ed: &mut Editor, open: &mut
Some(None) => {
ed.ask_stop = false;
ed.closing = false;
ed.quit = false;
}
None => {}
}
@@ -1000,7 +1022,10 @@ fn prompts(app: &mut SheetApp, ctx: &egui::Context, ed: &mut Editor, open: &mut
ed.ask_close = false;
*open = false;
}
Some(None) => ed.ask_close = false,
Some(None) => {
ed.ask_close = false;
ed.quit = false;
}
None => {}
}
}
+61
View File
@@ -403,6 +403,67 @@ fn the_editor_works_only_on_the_workbook_it_opened_in() {
assert!(h.state().dialog.as_ref().is_none_or(|d| d.name() != "macroEditor"));
}
/// Asks the window to close (the close button, Alt+F4, quitting) and returns the commands the
/// app answered with over the next frames.
fn request_window_close(h: &mut Harness) -> Vec<egui::ViewportCommand> {
h.input_mut().viewports.entry(egui::ViewportId::ROOT).or_default().events.push(egui::ViewportEvent::Close);
step_commands(h)
}
fn step_commands(h: &mut Harness) -> Vec<egui::ViewportCommand> {
let mut out = Vec::new();
for _ in 0..4 {
h.step();
out.extend(h.output().viewport_output.get(&egui::ViewportId::ROOT).map(|v| v.commands.clone()).unwrap_or_default());
}
out
}
#[test]
fn quitting_asks_about_the_editors_unsaved_steps_first() {
let mut h = harness();
three(&mut h);
h.state_mut().macros.editor.as_mut().unwrap().draft.name = "Unsaved".into();
h.state_mut().macros.editor.as_mut().unwrap().dirty = true;
// As if the workbook had just been saved.
let d = h.state_mut().session.active_mut().unwrap();
d.saved = d.wb.clone();
h.run_steps(2);
// Every workbook is saved, but the draft is not: the window stays open and the editor asks.
let sent = request_window_close(&mut h);
assert!(sent.contains(&egui::ViewportCommand::CancelClose), "{sent:?}");
assert!(!sent.contains(&egui::ViewportCommand::Close), "{sent:?}");
assert!(h.query_by_label_contains("Save changes to macro “Unsaved”").is_some());
// Cancel keeps the app and the draft.
h.get_by_label("Cancel").click();
let sent = step_commands(&mut h);
assert!(!sent.contains(&egui::ViewportCommand::Close), "{sent:?}");
assert_eq!(h.state().macros.editor.as_ref().map(|e| e.draft.name.as_str()), Some("Unsaved"));
// Asked again, Don't Save closes the editor and then the window.
request_window_close(&mut h);
h.get_by_label("Don't Save").click();
let sent = step_commands(&mut h);
assert!(h.state().macros.editor.is_none());
assert!(sent.contains(&egui::ViewportCommand::Close), "{sent:?}");
}
#[test]
fn quitting_with_the_editor_open_still_asks_about_unsaved_workbooks() {
let mut h = harness();
three(&mut h);
h.state_mut().run("cell.set", json!({"cell": "Z9", "input": "unsaved"})).unwrap();
h.run_steps(2);
let sent = request_window_close(&mut h);
assert!(sent.contains(&egui::ViewportCommand::CancelClose), "{sent:?}");
// The saved, unchanged editor closes and the window close goes on (the window then asks to
// close again, which a test harness has to do by hand); the workbook is asked about as
// without the editor.
assert!(h.state().macros.editor.is_none());
assert!(sent.contains(&egui::ViewportCommand::Close), "{sent:?}");
request_window_close(&mut h);
assert_eq!(h.state().dialog.as_ref().map(|d| d.name()), Some("saveChanges"));
}
#[test]
fn a_dialog_asked_for_over_the_editor_says_why_it_did_not_open() {
let mut h = harness();
+3 -1
View File
@@ -293,7 +293,9 @@ other macros open; a macro that was left out is not in the saved file.
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.
the editor. It keeps its window until it closes itself: another dialog asked for meanwhile is
refused with a message, and closing the app first asks about the editor's debug run and unsaved
steps, then about unsaved workbooks.
- **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