diff --git a/crates/keymap/README.md b/crates/keymap/README.md index 97da4352..2b156f6f 100644 --- a/crates/keymap/README.md +++ b/crates/keymap/README.md @@ -306,6 +306,7 @@ if let Some(state) = &mut self.shortcuts { let dialog = dialog.sets(&mut self.settings.shortcut_sets, &BUILTINS); match dialog.show(ctx, state) { Some(Outcome::Ok) => self.save_settings(), + // After each edit, and after a Cancel that undid edits. Some(Outcome::Changed) => self.rebuild_menus(), Some(Outcome::Import { formats }) => self.pick_import_file(formats), Some(Outcome::Export { name, json }) => self.save_file(&name, json), @@ -324,8 +325,8 @@ Outcomes: | Outcome | The app | |---|---| | `Ok` | Saves `Profiles` (with sets) or `keymap.overrides()`. | -| `Cancel` | Nothing to do: the keymap and the sets are as they were at open. | -| `Changed` | Updates its menus, or saves at once if it saves live. When a frame also has another outcome, `Changed` comes on the next frame. | +| `Cancel` | Nothing to save: the keymap and the sets are as they were at open. When Cancel undid edits, `Changed` follows. | +| `Changed` | Updates its menus, or saves at once if it saves live. It comes after each edit and after a Cancel that undid edits. When a frame also has another outcome, `Changed` comes on the next frame; `is_open()` stays true until it has. | | `Import { formats }` | Opens a file of one of the formats, then calls `import_json` (craft-keymap JSON, `ui::JSON_FORMAT`) or reads it into a `ForeignSet` and calls `review_import`. | | `Export { name, json }`, `ExportAs { format, name, keymap }` | Writes the set where the user chooses (a download on the web). | | `Summary { name, html, text }` | Shows, saves or prints the summary. | diff --git a/crates/keymap/src/ui/mod.rs b/crates/keymap/src/ui/mod.rs index cd476f37..cf73737f 100644 --- a/crates/keymap/src/ui/mod.rs +++ b/crates/keymap/src/ui/mod.rs @@ -34,7 +34,8 @@ use state::Env; pub enum Outcome { /// Closed with OK: save `Profiles` (with sets) or `keymap.overrides()`. Ok, - /// Closed with Cancel: the keymap and profiles are back as they were when it opened. + /// Closed with Cancel: the keymap and profiles are back as they were when it opened. When that + /// undid edits, `Changed` follows on the next frame. Cancel, /// An edit (for live menus and live saving). When a frame also has another outcome, such as /// `Import` after the Save changes prompt, `Changed` follows on the next frame. @@ -203,6 +204,9 @@ impl<'a> ShortcutsDialog<'a> { /// screen allows, at most 1320×900). Closing it with × is Cancel. pub fn show(self, ctx: &egui::Context, state: &mut DialogState) -> Option { self.with_env(|env| { + if !state.is_showing() { + return state.finish(); + } let title = env.text.visual(&env.text.ui(env.options.title)); let screen = ctx.content_rect().size(); let max = egui::vec2(1320.0, 900.0).min(screen - egui::vec2(16.0, 16.0)).max(egui::vec2(240.0, 180.0)); @@ -218,10 +222,13 @@ impl<'a> ShortcutsDialog<'a> { .min_size(min) .max_size(max) .show(ctx, |ui| out = frame(ui, env, state)); - if !open && state.is_open() { + if !open && state.is_showing() { state.cancel(env); out = state.finish(); } + if state.change_pending() { + ctx.request_repaint(); + } out }) } @@ -272,7 +279,7 @@ impl<'a> ShortcutsDialog<'a> { fn frame(ui: &mut egui::Ui, env: &mut Env, state: &mut DialogState) -> Option { state.begin(env); state.input(env, ui.ctx()); - if state.is_open() { + if state.is_showing() { view::body(ui, env, state); } let out = state.finish(); diff --git a/crates/keymap/src/ui/state.rs b/crates/keymap/src/ui/state.rs index fbb544a6..d9461647 100644 --- a/crates/keymap/src/ui/state.rs +++ b/crates/keymap/src/ui/state.rs @@ -264,8 +264,15 @@ impl DialogState { } } - /// False after OK or Cancel: drop the state. + /// False once the dialog has closed with OK or Cancel and reported its last outcome: drop the + /// state then. After a Cancel that undid edits it stays true for the frame that reports + /// [`Outcome::Changed`]. pub fn is_open(&self) -> bool { + self.phase != Phase::Closed || self.changed + } + + /// Whether the dialog is drawn: not yet closed with OK or Cancel. + pub(crate) fn is_showing(&self) -> bool { self.phase != Phase::Closed } @@ -698,14 +705,20 @@ impl DialogState { } /// Cancel, window ×, or Escape when idle: everything back as it was when the dialog opened. + /// When that undoes edits, Changed follows, so the app rebuilds its menus (and saves, if it + /// saved the edits live). pub(crate) fn cancel(&mut self, env: &mut Env) { + let mut undone = false; if let Some(opened) = self.opened.take() { + undone = *env.keymap.overrides() != opened.overrides; env.keymap.set_overrides(opened.overrides); if let (Some((p, _)), Some(profiles)) = (env.sets.as_mut(), opened.profiles) { + undone |= **p != profiles; **p = profiles; } } self.close(Outcome::Cancel); + self.changed = undone; } fn close(&mut self, outcome: Outcome) { @@ -1167,8 +1180,9 @@ mod tests { assert!(matches!(st.finish(), Some(Outcome::Changed))); st.escape(&mut fx.env()); assert!(matches!(st.finish(), Some(Outcome::Cancel))); - assert!(!st.is_open()); assert!(fx.keys("object.front").is_empty(), "Cancel restores the opening keymap"); + assert!(st.is_open() && matches!(st.finish(), Some(Outcome::Changed)), "Changed follows, for the app's menus"); + assert!(!st.is_open()); } #[test] diff --git a/crates/keymap/tests/ui_dialog.rs b/crates/keymap/tests/ui_dialog.rs index 8df42773..38a77e28 100644 --- a/crates/keymap/tests/ui_dialog.rs +++ b/crates/keymap/tests/ui_dialog.rs @@ -497,6 +497,9 @@ fn window_close_is_cancel() { click(&mut h, "Cancel"); assert!(h.state().canceled()); assert_eq!(h.state().bindings("file.save"), ["Cmd+S"]); + let after: Vec = h.state().outcomes.iter().skip_while(|o| !matches!(o, Outcome::Cancel)).map(|o| matches!(o, Outcome::Changed)).collect(); + assert_eq!(after, [false, true], "Changed follows Cancel, which undid an edit"); + assert!(!h.state().state.is_open()); } #[test]