forked from 1b-agent/opencadstudio-upstream
feat(clipboard): paste images from other programs on desktop too
A paste that finds no text now reads an image off the system clipboard on desktop as well as on the web. The pixels are written as a PNG beside the drawing (or in the config folder for a drawing never saved) and placed as a raster image at the cursor, the same way the web paste attaches a screenshot. PASTESPEC falls back to the same image paste when the clipboard holds neither objects nor text. Refs #645 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d4b4fc37e1
commit
11f68a1185
4 files changed
+62
-33
No files matched your search
+3
-5
@@ -2389,11 +2389,9 @@ pub enum Message {
|
||||
PasteSpecial(Option<String>),
|
||||
/// Completion of a system text clipboard read requested by PASTECLIP.
|
||||
SystemClipboardPaste(SystemClipboardText),
|
||||
/// Web: completion of the clipboard-image read the paste fallback starts
|
||||
/// when no text was found — `Some((name, bytes))` attaches the image,
|
||||
/// `None` reports the usual unsupported-content error. Native never
|
||||
/// constructs it.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Completion of the clipboard-image read the paste fallback starts when
|
||||
/// no text was found — `Some((name, bytes))` attaches the image, `None`
|
||||
/// reports the usual unsupported-content error.
|
||||
SystemClipboardPasteImage(Option<(String, Vec<u8>)>),
|
||||
/// Ctrl/Cmd+A — select all layer rows when the Layer Manager is open, or all
|
||||
/// drawing objects otherwise (#236).
|
||||
|
||||
@@ -2162,12 +2162,59 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a clipboard image read on the web (a pasted screenshot) the way
|
||||
/// the IMAGE command's pick does: register the bytes name-keyed, decode
|
||||
/// for the file's true pixel size, then commit the entity — but placed
|
||||
/// directly at the cursor (0,0 before any pointer move) with the command's
|
||||
/// Read an image off the system clipboard for a paste that found no
|
||||
/// text. The web gets the encoded file from the Clipboard API; native
|
||||
/// gets pixels, which are written as a PNG beside the drawing (or, for a
|
||||
/// drawing never saved, in the config folder) so the image entity keeps
|
||||
/// a file to point at.
|
||||
pub(in crate::app) fn read_system_clipboard_image(&self) -> Task<Message> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
Task::perform(
|
||||
crate::sys::read_clipboard_image(),
|
||||
Message::SystemClipboardPasteImage,
|
||||
)
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let folder = self.tabs[self.active_tab]
|
||||
.current_path
|
||||
.as_ref()
|
||||
.and_then(|path| path.parent().map(std::path::Path::to_path_buf))
|
||||
.filter(|folder| !folder.as_os_str().is_empty())
|
||||
.or_else(|| crate::config::config_dir().map(|dir| dir.join("pasted")));
|
||||
iced::clipboard::read_image().map(move |image| {
|
||||
let saved = image.ok().zip(folder.clone()).and_then(|(image, folder)| {
|
||||
let pixels = image::RgbaImage::from_raw(
|
||||
image.size.width,
|
||||
image.size.height,
|
||||
image.rgba.to_vec(),
|
||||
)?;
|
||||
let mut png = std::io::Cursor::new(Vec::new());
|
||||
pixels.write_to(&mut png, image::ImageFormat::Png).ok()?;
|
||||
let stamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |elapsed| elapsed.as_millis());
|
||||
let name = crate::sys::unique_pasted_image_name(
|
||||
&format!("pasted-{stamp}"),
|
||||
"png",
|
||||
|name| folder.join(name).exists(),
|
||||
);
|
||||
let path = folder.join(name);
|
||||
std::fs::create_dir_all(&folder).ok()?;
|
||||
std::fs::write(&path, png.get_ref()).ok()?;
|
||||
Some((path.to_string_lossy().into_owned(), png.into_inner()))
|
||||
});
|
||||
Message::SystemClipboardPasteImage(saved)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach a clipboard image (a pasted screenshot) the way the IMAGE
|
||||
/// command's pick does: register the bytes name-keyed, decode for the
|
||||
/// file's true pixel size, then commit the entity — but placed directly
|
||||
/// at the cursor (0,0 before any pointer move) with the command's
|
||||
/// default width, skipping the pick prompts a paste should not run.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(super) fn paste_clipboard_image(&mut self, name: String, bytes: Vec<u8>) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].is_start {
|
||||
|
||||
+7
-22
@@ -6895,11 +6895,10 @@ impl OpenCADStudio {
|
||||
self.push_ucs_to_cmd(i);
|
||||
Task::none()
|
||||
}
|
||||
// Nothing but maybe an image: paste it as one.
|
||||
None if self.clipboard.is_empty() => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.command_line
|
||||
.push_error(crate::tr!("clipboard", "no-supported-content").as_ref());
|
||||
Task::none()
|
||||
self.read_system_clipboard_image()
|
||||
}
|
||||
None => Task::none(),
|
||||
}
|
||||
@@ -6917,24 +6916,11 @@ impl OpenCADStudio {
|
||||
self.focus_cmd_input()
|
||||
}
|
||||
Text::EmptyOrUnsupported => {
|
||||
// Web: no text — the clipboard may still hold an IMAGE
|
||||
// (a pasted screenshot). Read it through the async
|
||||
// Clipboard API and attach it the way the IMAGE
|
||||
// command's pick does; only when that also finds
|
||||
// No text — the clipboard may still hold an IMAGE (a
|
||||
// pasted screenshot). Read it and attach it the way the
|
||||
// IMAGE command's pick does; only when that also finds
|
||||
// nothing does the unsupported-content error surface.
|
||||
// Native has no image route off the sync clipboard.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
return Task::perform(
|
||||
crate::sys::read_clipboard_image(),
|
||||
Message::SystemClipboardPasteImage,
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::tr!("clipboard", "no-supported-content").as_ref(),
|
||||
);
|
||||
Task::none()
|
||||
}
|
||||
self.read_system_clipboard_image()
|
||||
}
|
||||
Text::Unavailable => {
|
||||
self.command_line
|
||||
@@ -6954,10 +6940,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// Web: the clipboard-image fallback of a text-less paste either
|
||||
// The clipboard-image fallback of a text-less paste either
|
||||
// attaches the image or falls back to the same error the text
|
||||
// path reports when nothing readable was on the clipboard.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Message::SystemClipboardPasteImage(image) => match image {
|
||||
Some((name, bytes)) => self.paste_clipboard_image(name, bytes),
|
||||
None => {
|
||||
|
||||
@@ -440,7 +440,6 @@ fn pasted_image_name(mime_type: &str) -> String {
|
||||
/// the candidate already names a registered source. The taken-check is
|
||||
/// injectable so this — the part worth locking with tests — runs natively;
|
||||
/// wasm passes [`crate::scene::model::image_model::has_source`].
|
||||
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // tested everywhere, used on wasm
|
||||
pub(crate) fn unique_pasted_image_name(
|
||||
base: &str,
|
||||
ext: &str,
|
||||
|
||||
Reference in new issue
Block a user