fix(input): fire a rebound SPACE only from the command line

#1713 dispatched a captured SPACE as a shortcut whenever it was rebound,
so with SPACE on FINALIZE a space typed into any other text field (layer
names, Properties, find) also ran FINALIZE, and the space it typed stayed
in the command line. Ask whether the command input has focus before
running the binding, and drop the space the field already inserted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-11 23:38:06 +03:00
1 parent c56cdd6a26
commit 7e0866e647
3 files changed
+40 -67

No files matched your search

+6
View File
@@ -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 {
+20
View File
@@ -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,
+14 -67
View File
@@ -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<String, String> = 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));
}
}