diff --git a/src/app/mod.rs b/src/app/mod.rs index 2a720a39c..f9d2cc2f5 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2618,6 +2618,12 @@ pub enum Message { shortcut: String, extend_selection: bool, }, + /// A text field captured SPACE while it is rebound away from + /// COMMANDSPACE; run the binding only if the command input has focus. + CommandLineSpaceProbe, + /// The command input has focus: drop the space it just typed and run the + /// SPACE binding. + CommandLineSpaceResolved, /// A widget captured Up/Down; resolve it only if the command input owns /// keyboard focus. CommandLineArrowProbe { diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 5d020da01..e9fe0c46d 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -23,6 +23,8 @@ fn is_modal_blocked_key_msg(msg: &Message) -> bool { | Message::ArrowKeyPressed { .. } | Message::CommandLineArrowProbe { .. } | Message::CommandLineArrowResolved { .. } + | Message::CommandLineSpaceProbe + | Message::CommandLineSpaceResolved | Message::DynTabNext | Message::DynTabPrev | Message::MTextCaretMove(_) @@ -2314,6 +2316,24 @@ impl OpenCADStudio { )) } + Message::CommandLineSpaceProbe => iced::widget::operation::is_focused( + iced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID), + ) + .map(|focused| { + if focused { + Message::CommandLineSpaceResolved + } else { + Message::Noop + } + }), + + Message::CommandLineSpaceResolved => { + if self.command_line.input.ends_with(' ') { + self.command_line.input.pop(); + } + self.update(Message::ShortcutPressed("SPACE".to_string())) + } + Message::CommandLineArrowProbe { direction, extend_selection, diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 16a339f04..8a99d183b 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -171,29 +171,6 @@ fn shortcut_key_name(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Opt Some(parts.join("+")) } -/// Whether a key that resolved to the shortcut name `shortcut` should fire as -/// a shortcut for the given keyboard dispatch `status`. A focused command line -/// (Status::Captured) normally swallows a key so the text field can type it, -/// but SPACE must still dispatch so its binding runs (COMMANDSPACE by default, -/// FINALIZE if rebound) even while a free-text edit is in progress. To avoid -/// firing the default COMMANDSPACE every time a text field captures a space, a -/// Captured-space only dispatches when SPACE is rebound away from COMMANDSPACE -/// (tracked in a static, since this closure is a non-capturing fn pointer). (#1710) -fn should_dispatch_shortcut(status: iced::event::Status, shortcut: &str) -> bool { - dispatch_shortcut( - status, - shortcut, - crate::app::shortcuts::SPACE_REBOUND_OFF_COMMANDSPACE - .load(std::sync::atomic::Ordering::Relaxed), - ) -} - -fn dispatch_shortcut(status: iced::event::Status, shortcut: &str, space_rebound: bool) -> bool { - status == iced::event::Status::Ignored - || crate::app::shortcuts::is_global_key(shortcut) - || (status == iced::event::Status::Captured && shortcut == "SPACE" && space_rebound) -} - /// `ViewportRenderMode` enum carries the raw DXF integers, not a label, /// so wrap it locally with a friendly name renderer. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -2848,14 +2825,20 @@ impl OpenCADStudio { } } let shortcut = shortcut_key_name(&key, modifiers)?; - // When the command line has focus, a key that is bound - // to a shortcut still fires that shortcut unless it is - // the space key bound to COMMANDSPACE (the default), - // which the command line needs for typing. The binding - // check happens in the update handler so this closure - // stays a non-capturing fn pointer. (#1710) - should_dispatch_shortcut(status, &shortcut) - .then_some(Message::ShortcutPressed(shortcut)) + if status == Status::Ignored + || crate::app::shortcuts::is_global_key(&shortcut) + { + return Some(Message::ShortcutPressed(shortcut)); + } + // A text field captured the key. SPACE rebound away from + // COMMANDSPACE still fires from the command line, but + // only there: other fields keep typing spaces. This + // closure is a plain fn, so the binding is read from a + // flag and the focus is asked for in update. (#1710) + (shortcut == "SPACE" + && crate::app::shortcuts::SPACE_REBOUND_OFF_COMMANDSPACE + .load(std::sync::atomic::Ordering::Relaxed)) + .then_some(Message::CommandLineSpaceProbe) } _ => None, } @@ -4444,40 +4427,4 @@ mod tests { super::super::StartSection::default(), ); } - - #[test] - fn captured_space_dispatches_only_when_rebound_off_commandspace() { - // #1710: when the command line holds focus it reports Status::Captured - // and swallows a key so the text field can type it. A Captured-space - // must only fire as a shortcut when SPACE is rebound away from the - // default COMMANDSPACE (e.g. to FINALIZE); otherwise the default - // COMMANDSPACE would fire on every space typed into a text field. - // The pure helpers are tested here so no test touches the shared flag. - use crate::app::shortcuts::space_rebound_off_commandspace; - use iced::event::Status; - use rustc_hash::FxHashMap; - - let mut bindings: FxHashMap = FxHashMap::default(); - bindings.insert("SPACE".to_string(), "COMMANDSPACE".to_string()); - let rebound = space_rebound_off_commandspace(&bindings); - assert!(!rebound); - assert!( - !dispatch_shortcut(Status::Captured, "SPACE", rebound), - "a Captured-space bound to COMMANDSPACE must be typed, not dispatched" - ); - - bindings.insert("SPACE".to_string(), "FINALIZE".to_string()); - let rebound = space_rebound_off_commandspace(&bindings); - assert!(rebound); - assert!( - dispatch_shortcut(Status::Captured, "SPACE", rebound), - "a Captured-space rebound away from COMMANDSPACE must dispatch" - ); - - // A non-space captured key is typed, not dispatched as a shortcut. - assert!(!dispatch_shortcut(Status::Captured, "A", true)); - // Ignored keys always dispatch, regardless of the SPACE binding. - assert!(dispatch_shortcut(Status::Ignored, "SPACE", false)); - assert!(dispatch_shortcut(Status::Ignored, "F5", false)); - } }