From e7e759268ae50cf255a5fb2994685d0ea6118efe Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 25 Sep 2026 15:15:44 +0300 Subject: [PATCH] fix(paste): Ctrl+V over the command line still pastes copied objects #1481 skipped the shortcut whenever any text input had focus. The command line is focused almost always, so copied drawing objects no longer pasted with Ctrl+V. The probe now tells the command line apart from other fields: another field keeps Ctrl+V to itself; over the command line copied objects start PASTECLIP and plain text is left to the input's native paste; with nothing focused text goes to the command line as before. PASTECLIP goes through Message::Command again, which also closes the context menus. The tests use the codec crate name and cover the command-line case. Co-Authored-By: Claude Opus 5.5 --- src/app/mod.rs | 13 ++++++++-- src/app/update/command.rs | 53 ++++++++++++++++++++++++++------------- src/app/update/mod.rs | 39 +++++++++++++++++----------- 3 files changed, 71 insertions(+), 34 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 98e417437..6dc807c32 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2063,6 +2063,15 @@ pub enum ArrowKey { Right, } +/// The text input holding focus when Ctrl+V arrives (native only). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PasteFocus { + None, + CommandLine, + /// Any other text field (dialogs, panels). + Field, +} + #[derive(Debug, Clone)] pub enum Message { SpaceMouseWake, @@ -2106,8 +2115,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), + /// Which text input held focus when `PasteShortcut` arrived. + PasteShortcutResolved(PasteFocus), /// 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 7617d4357..2affd1888 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -70,22 +70,25 @@ fn polyline_vertex_count(entity: &AcadEntityType) -> Option { } } +/// Which text input, if any, holds focus when Ctrl+V arrives: a focused +/// text input pastes natively, so the shortcut must not paste again. #[cfg(not(target_arch = "wasm32"))] #[derive(Default)] -struct CheckTextInputFocused { - last_was_text_input: bool, - is_focused: bool, +struct PasteFocusProbe { + /// The id of the text input whose `focusable` call comes next. + pending: Option>, + focus: Option, } #[cfg(not(target_arch = "wasm32"))] -impl iced::advanced::widget::Operation for CheckTextInputFocused { +impl iced::advanced::widget::Operation for PasteFocusProbe { fn text_input( &mut self, - _id: Option<&iced::advanced::widget::Id>, + id: Option<&iced::advanced::widget::Id>, _bounds: iced::Rectangle, _state: &mut dyn iced::advanced::widget::operation::TextInput, ) { - self.last_was_text_input = true; + self.pending = Some(id.cloned()); } fn focusable( @@ -94,21 +97,35 @@ impl iced::advanced::widget::Operation for CheckTextInputFocused { _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 { + // A text input reports `text_input` then `focusable`; other + // focusables (text editors) only the latter. + let Some(id) = self.pending.take() else { return; + }; + if state.is_focused() { + let command_line = + iced::advanced::widget::Id::new(crate::ui::command_line::CMD_INPUT_ID); + self.focus = Some(if id.as_ref() == Some(&command_line) { + crate::app::PasteFocus::CommandLine + } else { + crate::app::PasteFocus::Field + }); } - operate(self); } - fn finish(&self) -> iced::advanced::widget::operation::Outcome { - iced::advanced::widget::operation::Outcome::Some(self.is_focused) + fn traverse( + &mut self, + operate: &mut dyn FnMut(&mut dyn iced::advanced::widget::Operation), + ) { + if self.focus.is_none() { + operate(self); + } + } + + fn finish(&self) -> iced::advanced::widget::operation::Outcome { + iced::advanced::widget::operation::Outcome::Some( + self.focus.unwrap_or(crate::app::PasteFocus::None), + ) } } @@ -1999,7 +2016,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } #[cfg(not(target_arch = "wasm32"))] { - iced::advanced::widget::operate(CheckTextInputFocused::default()) + iced::advanced::widget::operate(PasteFocusProbe::default()) .map(Message::PasteShortcutResolved) } #[cfg(target_arch = "wasm32")] diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 5be375d26..c458bf2f0 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -6106,15 +6106,19 @@ 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. + // A focused text input pastes text natively: another field keeps + // Ctrl+V to itself, and the command line must not get the text a + // second time. Copied objects still paste over the command line. + Message::PasteShortcutResolved(focus) => { + use super::PasteFocus; + if focus == PasteFocus::Field { + Task::none() + } else if !self.clipboard.is_empty() { + self.update(Message::Command("PASTECLIP".to_string())) + } else if focus == PasteFocus::CommandLine { Task::none() - } else if self.clipboard.is_empty() { - self.read_system_clipboard_for_paste() } else { - self.dispatch_command("PASTECLIP") + self.read_system_clipboard_for_paste() } } @@ -10983,25 +10987,32 @@ mod free_text_entry_tests { } #[test] - fn paste_shortcut_resolved_focused_is_noop() { + fn paste_shortcut_leaves_a_focused_field_alone() { let mut app = OpenCADStudio::new_for_test(); app.automation_op(r#"{"op":"new"}"#); app.command_line.input = "EXISTING".into(); + app.clipboard = vec![codec::EntityType::Line(codec::entities::Line::default())]; - // When a text input is focused, PasteShortcutResolved(true) does not mutate command line or start a command. - let _ = app.update(Message::PasteShortcutResolved(true)); + // A dialog field pasted natively: no command-line text, no PASTECLIP. + let _ = app.update(Message::PasteShortcutResolved(crate::app::PasteFocus::Field)); 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() { + fn paste_shortcut_over_the_command_line_pastes_copied_objects() { + use crate::app::PasteFocus; 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)); + // Text only: the command line already pasted it natively. + let _ = app.update(Message::PasteShortcutResolved(PasteFocus::CommandLine)); + assert!(app.command_line.input.is_empty()); + assert!(app.tabs[0].active_cmd.is_none()); + + // Copied objects paste even though the command line holds focus. + app.clipboard = vec![codec::EntityType::Line(codec::entities::Line::default())]; + let _ = app.update(Message::PasteShortcutResolved(PasteFocus::CommandLine)); assert!(app.tabs[0].active_cmd.as_ref().is_some_and(|cmd| cmd.name() == "PASTECLIP")); } }