keymap: report Changed after a Cancel that undid edits, so apps rebuild their menus

This commit is contained in:
aiongg committed 2026-10-20 15:08:46 +00:00
1 parent 2abdcd53ab
commit 0039a5c866
4 files changed
+32 -7

No files matched your search

+3 -2
View File
@@ -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. |
+10 -3
View File
@@ -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<Outcome> {
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<Outcome> {
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();
+16 -2
View File
@@ -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]
+3
View File
@@ -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<bool> = 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]