Preferences: a shortcut recorded from a search result takes the key (#10043) (#10058)

* Preferences: a shortcut recorded from a search result takes the key (#10043)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Preferences: a shortcut recorded from a search result takes the key (#10043)

A Preferences search lists matching shortcuts with their key buttons,
but a recording started there only counted while the Shortcuts page
itself was open, and the page skipped its recorder during a search: the
key went into the search box and ran its command behind Preferences.

keymap::capturing now counts a recording while a search shows, and the
recorder runs for it; clicking back into the search field stops the
recording, so the search keeps its own typing. The recording's message,
a taken key's Use anyway / Cancel choice and the next-step hint show
above the search's shortcut matches too (outcome, shared with the page).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
austin62jake
2026-10-31 13:28:40 +00:00
committed by GitHub
parent 6047c0d5a1
commit f0acea0992
+88 -18
View File
@@ -548,10 +548,15 @@ pub fn reset(app: &mut SolveApp, id: Option<&str>) {
pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
let ctx = ui.ctx().clone();
let filtering = crate::prefs_search::querying(ui);
// A Preferences search lists the matching shortcuts too, and one recorded from there takes
// the key like one on this page (#10043). Only a search field that has the keyboard keeps
// its keys: going back to it to type stops the recording.
if ctx.memory(|m| m.has_focus(egui::Id::new("prefs_search"))) && app.keymap.capturing.is_some() {
stop_capture(app);
}
// Keep a pending chord until it has settled and all keys are released. Modifier-only
// input never starts the timer, so modifiers can be assembled at the user's own pace.
// A Preferences search is using the keyboard; don't swallow those keys to finish a capture.
if !filtering && let Some(id) = app.keymap.capturing.clone() {
if let Some(id) = app.keymap.capturing.clone() {
match ctx.input(|i| app.keymap.recorder.update(i)) {
Recording::Waiting => {
if app.keymap.recorder.key.is_some() {
@@ -627,20 +632,8 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
},
));
}
if let Some(m) = &app.keymap.message {
ui_kit::text(ui, m, ui_kit::Text::Report("keymap_message", true));
}
if let Some((_, key, _, _)) = app.keymap.conflict.clone() {
ui.horizontal_wrapped(|ui| {
let replace = ui_kit::button(ui, crate::i18n::format("keymap.use_anyway", &[("key", &display(&key))]), ui_kit::Button::Normal);
crate::scenario::publish_target("keymap:replace", replace.rect);
if replace.clicked() {
action = Some((String::new(), "replace"));
}
if ui_kit::button(ui, app.tr_label("Cancel"), ui_kit::Button::Normal).clicked() {
action = Some((String::new(), "cancel"));
}
});
if let Some(chosen) = outcome(app, ui) {
action = Some((String::new(), chosen));
}
ui_kit::text(ui, app.tr_label("Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation)."), ui_kit::Text::Note("navigation"));
}
@@ -702,6 +695,16 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
ui_kit::Column::new("Keyboard Shortcuts", 1.0).flex().min_content_width(key_width),
ui_kit::Column::new("", 0.0).icon_only().min_content_width(action_width),
];
// A search shows a recording's outcome and its next step above the shortcuts it found.
if filtering {
if let Some(chosen) = outcome(app, ui) {
action = Some((String::new(), chosen));
}
if app.keymap.capturing.is_some() {
let hint = ctx.input(|input| app.keymap.recorder.hint(input));
ui_kit::text(ui, app.tr(hint), ui_kit::Text::Hint("shortcut_capture_state"));
}
}
ui_kit::table(ui, "keymap", &columns, crate::i18n::tr("keymap.empty"), |ui, table| {
for (c, keys, translated_label) in commands {
table.row_visible(ui, c.id, app.keymap.capturing.as_deref() == Some(c.id), false, |row| {
@@ -826,6 +829,28 @@ pub fn page(app: &mut SolveApp, ui: &mut egui::Ui) {
}
}
/// A recording's outcome: its message, and for a key another command uses, Replace and Cancel
/// (the one chosen).
fn outcome(app: &SolveApp, ui: &mut egui::Ui) -> Option<&'static str> {
let mut chosen = None;
if let Some(m) = &app.keymap.message {
ui_kit::text(ui, m, ui_kit::Text::Report("keymap_message", true));
}
if let Some((_, key, _, _)) = app.keymap.conflict.clone() {
ui.horizontal_wrapped(|ui| {
let replace = ui_kit::button(ui, crate::i18n::format("keymap.use_anyway", &[("key", &display(&key))]), ui_kit::Button::Normal);
crate::scenario::publish_target("keymap:replace", replace.rect);
if replace.clicked() {
chosen = Some("replace");
}
if ui_kit::button(ui, app.tr_label("Cancel"), ui_kit::Button::Normal).clicked() {
chosen = Some("cancel");
}
});
}
chosen
}
/// Stop waiting for a key (the dialog closed).
pub fn stop_capture(app: &mut SolveApp) {
app.keymap.capturing = None;
@@ -834,9 +859,13 @@ pub fn stop_capture(app: &mut SolveApp) {
app.keymap.message = None;
}
/// Is the page waiting for a key (so the key must not also run a command)?
/// Is the page waiting for a key (so the key must not also run a command)? On the Shortcuts page,
/// or in a Preferences search, which shows the matching shortcuts on the page it started from
/// (#10043).
pub fn capturing(app: &SolveApp) -> bool {
app.prefs_window.open && app.prefs_window.section == crate::prefs::PAGE_SHORTCUTS && app.keymap.capturing.is_some()
app.prefs_window.open
&& app.keymap.capturing.is_some()
&& (app.prefs_window.section == crate::prefs::PAGE_SHORTCUTS || !app.prefs_window.filter.trim().is_empty())
}
#[cfg(test)]
@@ -1162,6 +1191,47 @@ mod tests {
assert!(h.app.prefs_window.filter.is_empty(), "a shortcut being recorded keeps the key");
}
/// A shortcut recorded from a Preferences search result takes the key, as on the Shortcuts
/// page (#10043): the search keeps its query, and the key runs no command behind
/// Preferences (P is Project's, which would open Create Sketch). P being Project's is
/// reported there with the choice to use it anyway, as on the page. Going back to the search
/// field to type stops a recording.
#[test]
fn a_shortcut_recorded_from_a_preferences_search_takes_the_key() {
let mut h = crate::scenario::Harness::new();
h.app.home.open = false;
h.call("ui.prefs", json!({"open": "General"}));
h.frames(3);
h.step(&json!({"text": "fix"})).expect("search");
h.frames(3);
h.step(&json!({"click": {"handle": "prefs:search:shortcuts"}})).expect("unfold the shortcut matches");
h.frames(2);
h.step(&json!({"click": {"handle": "shortcut:sketch.constraint.fix"}})).expect("record Fix/UnFix's key");
assert!(capturing(&h.app), "recording from a search result");
h.step(&json!({"key": "P", "pressed": true})).unwrap();
h.step(&json!({"text": "p"})).unwrap();
h.step(&json!({"key": "P", "pressed": false})).unwrap();
h.frames(65);
assert_eq!(h.app.prefs_window.filter, "fix", "the key went to the search");
assert!(h.app.dialog.is_none() && h.app.session.active_sketch.is_none(), "the key ran a command");
assert!(h.app.keymap.capturing.is_none());
let conflict = h.app.keymap.conflict.as_ref().map(|(id, key, ..)| (id.as_str(), key.as_str()));
assert_eq!(conflict, Some(("sketch.constraint.fix", "P")), "{:?}", h.app.keymap.message);
// The search shows why, and the choice, above the shortcuts it found.
let message = h.app.keymap.message.clone().unwrap_or_default();
assert!(h.widget(&message).is_some() && h.widget("Use P here anyway").is_some(), "{message}");
h.step(&json!({"click": {"handle": "keymap:replace"}})).expect("use P anyway");
assert_eq!(effective(&h.app, "sketch.constraint.fix").as_deref(), Some("P"));
h.step(&json!({"click": {"handle": "shortcut:sketch.constraint.fix:1"}})).expect("record a second key");
assert!(capturing(&h.app));
h.step(&json!({"click": {"handle": "prefs:search"}})).expect("back to the search");
h.step(&json!({"text": "x"})).unwrap();
h.frames(2);
assert!(h.app.keymap.capturing.is_none(), "typing in the search stops the recording");
assert_eq!(h.app.prefs_window.filter, "fixx");
}
#[test]
fn escape_cancels_only_recording_and_no_command_fires_during_capture() {
let mut h = crate::scenario::Harness::new();