diff --git a/src/app/mod.rs b/src/app/mod.rs index f6c15b672..730e2a48e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2096,6 +2096,8 @@ pub enum Message { /// Ctrl+V. Routed by `update` into an open text editor, the drawing-object /// clipboard, or the system text clipboard. PasteShortcut, + /// Result of probing whether a text input was focused when `PasteShortcut` arrived. + PasteShortcutResolved(bool), /// Completion of a system text clipboard read requested by PASTECLIP. SystemClipboardPaste(SystemClipboardText), /// Ctrl/Cmd+A — select all layer rows when the Layer Manager is open, or all diff --git a/src/app/update/command.rs b/src/app/update/command.rs index c6d86d326..101f4d77d 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -70,6 +70,48 @@ fn polyline_vertex_count(entity: &AcadEntityType) -> Option { } } +#[cfg(not(target_arch = "wasm32"))] +#[derive(Default)] +struct CheckTextInputFocused { + last_was_text_input: bool, + is_focused: bool, +} + +#[cfg(not(target_arch = "wasm32"))] +impl iced::advanced::widget::Operation for CheckTextInputFocused { + fn text_input( + &mut self, + _id: Option<&iced::advanced::widget::Id>, + _bounds: iced::Rectangle, + _state: &mut dyn iced::advanced::widget::operation::TextInput, + ) { + self.last_was_text_input = true; + } + + fn focusable( + &mut self, + _id: Option<&iced::advanced::widget::Id>, + _bounds: iced::Rectangle, + state: &mut dyn iced::advanced::widget::operation::Focusable, + ) { + let is_text = std::mem::replace(&mut self.last_was_text_input, false); + if is_text && state.is_focused() { + self.is_focused = true; + } + } + + fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn iced::advanced::widget::Operation)) { + if self.is_focused { + return; + } + operate(self); + } + + fn finish(&self) -> iced::advanced::widget::operation::Outcome { + iced::advanced::widget::operation::Outcome::Some(self.is_focused) + } +} + impl OpenCADStudio { pub(super) fn begin_tab_close_queue(&mut self, tab_ids: Vec) -> Task { self.pending_tab_closes.clear(); @@ -1955,10 +1997,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { #[cfg(not(target_arch = "wasm32"))] return Task::none(); } - if self.clipboard.is_empty() { - self.read_system_clipboard_for_paste() - } else { - Task::done(Message::Command("PASTECLIP".to_string())) + #[cfg(not(target_arch = "wasm32"))] + { + iced::advanced::widget::operate(CheckTextInputFocused::default()) + .map(Message::PasteShortcutResolved) + } + #[cfg(target_arch = "wasm32")] + { + if self.clipboard.is_empty() { + self.read_system_clipboard_for_paste() + } else { + Task::done(Message::Command("PASTECLIP".to_string())) + } } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index d5da6dcb0..6b2cb611c 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -6104,6 +6104,18 @@ impl OpenCADStudio { // take priority and system text is used when that clipboard is empty. Message::PasteShortcut => self.on_paste_shortcut(), + Message::PasteShortcutResolved(focused) => { + if focused { + // A focused text_input widget already handled Ctrl+V natively. + // Doing anything here would duplicate the paste or leak text into the command line. + Task::none() + } else if self.clipboard.is_empty() { + self.read_system_clipboard_for_paste() + } else { + self.dispatch_command("PASTECLIP") + } + } + Message::SystemClipboardPaste(result) => { use super::SystemClipboardText as Text; @@ -10967,6 +10979,29 @@ mod free_text_entry_tests { let _ = app.update(Message::CommandInput(">Plugin MixedCase".into())); assert_eq!(app.command_line.input, ">Plugin MixedCase"); } + + #[test] + fn paste_shortcut_resolved_focused_is_noop() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.command_line.input = "EXISTING".into(); + + // When a text input is focused, PasteShortcutResolved(true) does not mutate command line or start a command. + let _ = app.update(Message::PasteShortcutResolved(true)); + assert_eq!(app.command_line.input, "EXISTING"); + assert!(app.tabs[0].active_cmd.is_none()); + } + + #[test] + fn paste_shortcut_resolved_unfocused_with_entities_starts_pasteclip() { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.clipboard = vec![acadrust::EntityType::Line(acadrust::entities::Line::default())]; + + // When unfocused and CAD entities are copied, dispatches PASTECLIP + let _ = app.update(Message::PasteShortcutResolved(false)); + assert!(app.tabs[0].active_cmd.as_ref().is_some_and(|cmd| cmd.name() == "PASTECLIP")); + } } /// Set or clear one bit of `SELECTIONPREVIEW`.