fix(ui): refine command line and clipboard

- preserve command history colors after reopening
- remove collapsed command bar residue and split the unsaved prompt
- read system text when the drawing clipboard is empty

Refs #467
Refs #468
Refs #470
This commit is contained in:
Hakan Seven 2026-08-04 18:53:08 +03:00
commit 9cf0c9f945
14 changed files with 245 additions and 40 deletions

View file

@ -193,7 +193,7 @@ impl OpenCADStudio {
"PASTECLIP" => {
if self.clipboard.is_empty() {
self.command_line.push_error(crate::t!("Clipboard is empty.").as_ref());
return Some(self.read_system_clipboard_for_paste());
} else {
let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard);
let base = self.clipboard_base;

View file

@ -1586,10 +1586,11 @@ pub enum Message {
crate::scene::text::web_font::Script,
Result<(), String>,
),
/// Ctrl+V. Routed by `update`: into the open text/MText editor (via an async
/// system-clipboard read, which is the only paste path that works on the
/// web) or, with no editor open, the entity paste command.
/// Ctrl+V. Routed by `update` into an open text editor, the drawing-object
/// clipboard, or the system text clipboard.
PasteShortcut,
/// 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
/// drawing objects otherwise (#236).
SelectAllShortcut,
@ -2791,6 +2792,15 @@ pub enum Message {
),
}
#[derive(Debug, Clone)]
pub enum SystemClipboardText {
Text(String),
EmptyOrUnsupported,
Unavailable,
Occupied,
ConversionFailed,
}
impl OpenCADStudio {
/// Install the Start-page video list, decoding each thumbnail's JPEG into
/// an image Handle exactly once (a fresh Handle per view frame would

View file

@ -1259,7 +1259,44 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
#[cfg(not(target_arch = "wasm32"))]
return Task::none();
}
Task::done(Message::Command("PASTECLIP".to_string()))
if self.clipboard.is_empty() {
self.read_system_clipboard_for_paste()
} else {
Task::done(Message::Command("PASTECLIP".to_string()))
}
}
pub(in crate::app) fn read_system_clipboard_for_paste(&self) -> Task<Message> {
#[cfg(target_arch = "wasm32")]
{
Task::perform(crate::sys::read_clipboard_text(), |text| {
Message::SystemClipboardPaste(match text {
Some(text) if !text.is_empty() => {
crate::app::SystemClipboardText::Text(text)
}
_ => crate::app::SystemClipboardText::EmptyOrUnsupported,
})
})
}
#[cfg(not(target_arch = "wasm32"))]
{
iced::clipboard::read_text().map(|result| {
use crate::app::SystemClipboardText as Text;
use iced::clipboard::Error;
let result = match result {
Ok(text) if !text.is_empty() => Text::Text((*text).clone()),
Ok(_) | Err(Error::ContentNotAvailable) => Text::EmptyOrUnsupported,
Err(Error::ClipboardUnavailable) | Err(Error::Unknown { .. }) => {
Text::Unavailable
}
Err(Error::ClipboardOccupied) => Text::Occupied,
Err(Error::ConversionFailure) => Text::ConversionFailed,
};
Message::SystemClipboardPaste(result)
})
}
}
pub(super) fn on_qselect_open(&mut self) -> Task<Message> {

View file

@ -3585,10 +3585,44 @@ impl OpenCADStudio {
// Ctrl+V. The MText editor and (on the web) the TEXT editor read the
// system clipboard asynchronously — the only paste path that works
// in the browser, where the synchronous clipboard the iced
// text_input expects is empty. With no editor open it falls through
// to the entity paste command.
// text_input expects is empty. With no editor open, drawing objects
// take priority and system text is used when that clipboard is empty.
Message::PasteShortcut => self.on_paste_shortcut(),
Message::SystemClipboardPaste(result) => {
use super::SystemClipboardText as Text;
match result {
Text::Text(text) => {
let flat = text.replace(['\r', '\n'], " ").to_uppercase();
self.command_line.input.push_str(&flat);
self.command_line.autocomplete_cursor = None;
self.focus_cmd_input()
}
Text::EmptyOrUnsupported => {
self.command_line.push_error(
crate::tr!("clipboard-no-supported-content").as_ref(),
);
Task::none()
}
Text::Unavailable => {
self.command_line
.push_error(crate::tr!("clipboard-unavailable").as_ref());
Task::none()
}
Text::Occupied => {
self.command_line
.push_error(crate::tr!("clipboard-occupied").as_ref());
Task::none()
}
Text::ConversionFailed => {
self.command_line
.push_error(crate::tr!("clipboard-conversion-failed").as_ref());
Task::none()
}
}
}
Message::SelectAllShortcut => {
let i = self.active_tab;
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {

View file

@ -1475,14 +1475,13 @@ fn unsaved_changes_dialog_window(
name: &str,
sizing: crate::ui::modal::ModalSizing,
) -> Element<'static, Message> {
let body_text = t!(
"Do you want to save changes to \"%{name}\"?",
name = name
);
container(
column![
text(body_text).size(13),
text(name.to_owned()).size(14),
iced::widget::Space::new().height(4),
text(crate::tr!("modal-unsaved-save-prompt"))
.size(12)
.style(dialog_muted_text_style),
iced::widget::Space::new().height(20),
row![
dialog_button(t!("Save"), Message::UnsavedDialogSave, button::primary),