forked from 1b-agent/opencadstudio-upstream
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c04fb8bd1
commit
e7e759268a
3 files changed
+71
-34
No files matched your search
+11
-2
@@ -2063,6 +2063,15 @@ pub enum ArrowKey {
|
|||||||
Right,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
SpaceMouseWake,
|
SpaceMouseWake,
|
||||||
@@ -2106,8 +2115,8 @@ pub enum Message {
|
|||||||
/// Ctrl+V. Routed by `update` into an open text editor, the drawing-object
|
/// Ctrl+V. Routed by `update` into an open text editor, the drawing-object
|
||||||
/// clipboard, or the system text clipboard.
|
/// clipboard, or the system text clipboard.
|
||||||
PasteShortcut,
|
PasteShortcut,
|
||||||
/// Result of probing whether a text input was focused when `PasteShortcut` arrived.
|
/// Which text input held focus when `PasteShortcut` arrived.
|
||||||
PasteShortcutResolved(bool),
|
PasteShortcutResolved(PasteFocus),
|
||||||
/// Completion of a system text clipboard read requested by PASTECLIP.
|
/// Completion of a system text clipboard read requested by PASTECLIP.
|
||||||
SystemClipboardPaste(SystemClipboardText),
|
SystemClipboardPaste(SystemClipboardText),
|
||||||
/// Ctrl/Cmd+A — select all layer rows when the Layer Manager is open, or all
|
/// Ctrl/Cmd+A — select all layer rows when the Layer Manager is open, or all
|
||||||
|
|||||||
+35
-18
@@ -70,22 +70,25 @@ fn polyline_vertex_count(entity: &AcadEntityType) -> Option<usize> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct CheckTextInputFocused {
|
struct PasteFocusProbe {
|
||||||
last_was_text_input: bool,
|
/// The id of the text input whose `focusable` call comes next.
|
||||||
is_focused: bool,
|
pending: Option<Option<iced::advanced::widget::Id>>,
|
||||||
|
focus: Option<crate::app::PasteFocus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
impl iced::advanced::widget::Operation<bool> for CheckTextInputFocused {
|
impl iced::advanced::widget::Operation<crate::app::PasteFocus> for PasteFocusProbe {
|
||||||
fn text_input(
|
fn text_input(
|
||||||
&mut self,
|
&mut self,
|
||||||
_id: Option<&iced::advanced::widget::Id>,
|
id: Option<&iced::advanced::widget::Id>,
|
||||||
_bounds: iced::Rectangle,
|
_bounds: iced::Rectangle,
|
||||||
_state: &mut dyn iced::advanced::widget::operation::TextInput,
|
_state: &mut dyn iced::advanced::widget::operation::TextInput,
|
||||||
) {
|
) {
|
||||||
self.last_was_text_input = true;
|
self.pending = Some(id.cloned());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focusable(
|
fn focusable(
|
||||||
@@ -94,21 +97,35 @@ impl iced::advanced::widget::Operation<bool> for CheckTextInputFocused {
|
|||||||
_bounds: iced::Rectangle,
|
_bounds: iced::Rectangle,
|
||||||
state: &mut dyn iced::advanced::widget::operation::Focusable,
|
state: &mut dyn iced::advanced::widget::operation::Focusable,
|
||||||
) {
|
) {
|
||||||
let is_text = std::mem::replace(&mut self.last_was_text_input, false);
|
// A text input reports `text_input` then `focusable`; other
|
||||||
if is_text && state.is_focused() {
|
// focusables (text editors) only the latter.
|
||||||
self.is_focused = true;
|
let Some(id) = self.pending.take() else {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn iced::advanced::widget::Operation<bool>)) {
|
|
||||||
if self.is_focused {
|
|
||||||
return;
|
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<bool> {
|
fn traverse(
|
||||||
iced::advanced::widget::operation::Outcome::Some(self.is_focused)
|
&mut self,
|
||||||
|
operate: &mut dyn FnMut(&mut dyn iced::advanced::widget::Operation<crate::app::PasteFocus>),
|
||||||
|
) {
|
||||||
|
if self.focus.is_none() {
|
||||||
|
operate(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self) -> iced::advanced::widget::operation::Outcome<crate::app::PasteFocus> {
|
||||||
|
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<Message> {
|
|||||||
}
|
}
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
iced::advanced::widget::operate(CheckTextInputFocused::default())
|
iced::advanced::widget::operate(PasteFocusProbe::default())
|
||||||
.map(Message::PasteShortcutResolved)
|
.map(Message::PasteShortcutResolved)
|
||||||
}
|
}
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
|||||||
+25
-14
@@ -6106,15 +6106,19 @@ impl OpenCADStudio {
|
|||||||
// take priority and system text is used when that clipboard is empty.
|
// take priority and system text is used when that clipboard is empty.
|
||||||
Message::PasteShortcut => self.on_paste_shortcut(),
|
Message::PasteShortcut => self.on_paste_shortcut(),
|
||||||
|
|
||||||
Message::PasteShortcutResolved(focused) => {
|
// A focused text input pastes text natively: another field keeps
|
||||||
if focused {
|
// Ctrl+V to itself, and the command line must not get the text a
|
||||||
// A focused text_input widget already handled Ctrl+V natively.
|
// second time. Copied objects still paste over the command line.
|
||||||
// Doing anything here would duplicate the paste or leak text into 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()
|
Task::none()
|
||||||
} else if self.clipboard.is_empty() {
|
|
||||||
self.read_system_clipboard_for_paste()
|
|
||||||
} else {
|
} else {
|
||||||
self.dispatch_command("PASTECLIP")
|
self.read_system_clipboard_for_paste()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10983,25 +10987,32 @@ mod free_text_entry_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn paste_shortcut_resolved_focused_is_noop() {
|
fn paste_shortcut_leaves_a_focused_field_alone() {
|
||||||
let mut app = OpenCADStudio::new_for_test();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
app.automation_op(r#"{"op":"new"}"#);
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
app.command_line.input = "EXISTING".into();
|
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.
|
// A dialog field pasted natively: no command-line text, no PASTECLIP.
|
||||||
let _ = app.update(Message::PasteShortcutResolved(true));
|
let _ = app.update(Message::PasteShortcutResolved(crate::app::PasteFocus::Field));
|
||||||
assert_eq!(app.command_line.input, "EXISTING");
|
assert_eq!(app.command_line.input, "EXISTING");
|
||||||
assert!(app.tabs[0].active_cmd.is_none());
|
assert!(app.tabs[0].active_cmd.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
app.automation_op(r#"{"op":"new"}"#);
|
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
|
// Text only: the command line already pasted it natively.
|
||||||
let _ = app.update(Message::PasteShortcutResolved(false));
|
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"));
|
assert!(app.tabs[0].active_cmd.as_ref().is_some_and(|cmd| cmd.name() == "PASTECLIP"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user