1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
15#![warn(unused_extern_crates)]
16#![warn(missing_docs)]
17
18use std::{collections::HashMap, fmt, path::PathBuf, sync::Arc};
19
20use zng_app::{command, event::*, shortcut::*, update::UPDATES, view_process::VIEW_PROCESS};
21use zng_ext_image::{IMAGES, ImageEntry, ImageVar};
22use zng_task::channel::{ChannelError, IpcBytes};
23use zng_txt::{ToTxt, Txt};
24use zng_var::{ResponseVar, response_var};
25use zng_view_api::{clipboard::ClipboardError as ViewError, image::ImageDecoded};
26use zng_wgt::{CommandIconExt as _, ICONS, wgt_fn};
27
28pub use zng_view_api::clipboard::{ClipboardType, ClipboardTypes};
29
30#[derive(Debug, Clone, PartialEq)]
34#[non_exhaustive]
35pub enum ClipboardError {
36 Disconnected,
41 NotSupported,
43 ImageNotLoaded,
45 Other(Txt),
49}
50impl std::error::Error for ClipboardError {}
51impl fmt::Display for ClipboardError {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 ClipboardError::Disconnected => write!(f, "no view-process available to process the request"),
55 ClipboardError::NotSupported => write!(f, "view-process or operating system does not support the data type"),
56 ClipboardError::ImageNotLoaded => write!(
57 f,
58 "cannot set image in clipboard because it has not finished loading or loaded with error"
59 ),
60 ClipboardError::Other(e) => write!(f, "{e}"),
61 }
62 }
63}
64impl From<ChannelError> for ClipboardError {
65 fn from(e: ChannelError) -> Self {
66 match e {
67 ChannelError::Disconnected { .. } => ClipboardError::Disconnected,
68 e => ClipboardError::Other(e.to_txt()),
69 }
70 }
71}
72impl From<ViewError> for ClipboardError {
73 fn from(e: ViewError) -> Self {
74 match e {
75 ViewError::NotSupported => ClipboardError::NotSupported,
76 e => ClipboardError::Other(e.to_txt()),
77 }
78 }
79}
80
81#[derive(Default)]
82struct ClipboardService {
83 update_text: std::sync::Weak<Result<Option<Txt>, ClipboardError>>,
84 update_image: std::sync::Weak<Result<Option<ImageVar>, ClipboardError>>,
85 update_paths: std::sync::Weak<Result<Option<Vec<PathBuf>>, ClipboardError>>,
86 update_exts: HashMap<Txt, std::sync::Weak<Result<Option<IpcBytes>, ClipboardError>>>,
87}
88
89app_local! {
90 static CLIPBOARD_SV: ClipboardService = ClipboardService::default();
91}
92
93pub struct CLIPBOARD;
102impl CLIPBOARD {
103 pub fn text(&self) -> Result<Option<Txt>, ClipboardError> {
105 let mut s = CLIPBOARD_SV.write();
106
107 match s.update_text.upgrade() {
108 Some(r) => (*r).clone(),
110 None => {
111 if !VIEW_PROCESS.is_available() {
113 return Err(ClipboardError::Disconnected);
114 }
115 let r = match VIEW_PROCESS.clipboard()?.read_text()? {
116 Ok(r) => Ok(Some(r)),
117 Err(e) => match e {
118 ViewError::NotFound => Ok(None),
119 ViewError::NotSupported => Err(ClipboardError::NotSupported),
120 e => Err(ClipboardError::Other(e.to_txt())),
121 },
122 };
123
124 let arc = Arc::new(r.clone());
126 s.update_text = Arc::downgrade(&arc);
127 UPDATES.once_update("", || {
128 let _hold = arc;
129 });
130
131 r
132 }
133 }
134 }
135 pub fn set_text(&self, txt: impl Into<Txt>) -> ResponseVar<Result<(), ClipboardError>> {
139 self.set_text_impl(txt.into())
140 }
141 fn set_text_impl(&self, txt: Txt) -> ResponseVar<Result<(), ClipboardError>> {
142 let (r, rsp) = response_var();
143 UPDATES.once_update("CLIPBOARD.set_text", move || {
144 if !VIEW_PROCESS.is_available() {
145 return r.respond(Err(ClipboardError::Disconnected));
146 }
147 match VIEW_PROCESS.clipboard() {
148 Ok(c) => match c.write_text(txt) {
149 Ok(vr) => r.respond(vr.map_err(ClipboardError::from)),
150 Err(e) => r.respond(Err(e.into())),
151 },
152 Err(e) => r.respond(Err(e.into())),
153 }
154 });
155 rsp
156 }
157
158 pub fn image(&self) -> Result<Option<ImageVar>, ClipboardError> {
164 let mut s = CLIPBOARD_SV.write();
165
166 match s.update_image.upgrade() {
167 Some(r) => (*r).clone(),
168 None => {
169 if !VIEW_PROCESS.is_available() {
170 return Err(ClipboardError::Disconnected);
171 }
172 let r = match VIEW_PROCESS.clipboard()?.read_image()? {
173 Ok(r) => {
174 let r = IMAGES.register(None, (r, ImageDecoded::default()));
175 Ok(Some(r))
176 }
177 Err(e) => match e {
178 ViewError::NotFound => Ok(None),
179 ViewError::NotSupported => Err(ClipboardError::NotSupported),
180 e => Err(ClipboardError::Other(e.to_txt())),
181 },
182 };
183
184 let arc = Arc::new(r.clone());
185 s.update_image = Arc::downgrade(&arc);
186 UPDATES.once_update("", || {
187 let _hold = arc;
188 });
189
190 r
191 }
192 }
193 }
194
195 pub fn set_image(&self, img: ImageEntry) -> ResponseVar<Result<(), ClipboardError>> {
199 let (r, rsp) = response_var();
200 UPDATES.once_update("CLIPBOARD.set_image", move || {
201 if !VIEW_PROCESS.is_available() {
202 return r.respond(Err(ClipboardError::Disconnected));
203 }
204 match VIEW_PROCESS.clipboard() {
205 Ok(c) => {
206 if img.is_loaded() {
207 match c.write_image(img.view_handle()) {
208 Ok(vr) => r.respond(vr.map_err(ClipboardError::from)),
209 Err(e) => r.respond(Err(e.into())),
210 }
211 } else {
212 r.respond(Err(ClipboardError::ImageNotLoaded));
213 }
214 }
215 Err(e) => r.respond(Err(e.into())),
216 }
217 });
218 rsp
219 }
220
221 pub fn paths(&self) -> Result<Option<Vec<PathBuf>>, ClipboardError> {
223 let mut s = CLIPBOARD_SV.write();
224
225 match s.update_paths.upgrade() {
226 Some(r) => (*r).clone(),
227 None => {
228 if !VIEW_PROCESS.is_available() {
229 return Err(ClipboardError::Disconnected);
230 }
231 let r = match VIEW_PROCESS.clipboard()?.read_paths()? {
232 Ok(r) => Ok(Some(r)),
233 Err(e) => match e {
234 ViewError::NotFound => Ok(None),
235 ViewError::NotSupported => Err(ClipboardError::NotSupported),
236 e => Err(ClipboardError::Other(e.to_txt())),
237 },
238 };
239
240 let arc = Arc::new(r.clone());
241 s.update_paths = Arc::downgrade(&arc);
242 UPDATES.once_update("", || {
243 let _hold = arc;
244 });
245
246 r
247 }
248 }
249 }
250
251 pub fn set_paths(&self, list: impl Into<Vec<PathBuf>>) -> ResponseVar<Result<(), ClipboardError>> {
255 self.set_paths_impl(list.into())
256 }
257 fn set_paths_impl(&self, list: Vec<PathBuf>) -> ResponseVar<Result<(), ClipboardError>> {
258 let (r, rsp) = response_var();
259 UPDATES.once_update("CLIPBOARD.set_paths", move || {
260 if !VIEW_PROCESS.is_available() {
261 return r.respond(Err(ClipboardError::Disconnected));
262 }
263 match VIEW_PROCESS.clipboard() {
264 Ok(c) => match c.write_paths(list) {
265 Ok(vr) => r.respond(vr.map_err(ClipboardError::from)),
266 Err(e) => r.respond(Err(e.into())),
267 },
268 Err(e) => r.respond(Err(e.into())),
269 }
270 });
271 rsp
272 }
273
274 pub fn extension(&self, data_type: impl Into<Txt>) -> Result<Option<IpcBytes>, ClipboardError> {
278 self.extension_impl(data_type.into())
279 }
280 fn extension_impl(&self, data_type: Txt) -> Result<Option<IpcBytes>, ClipboardError> {
281 let mut s = CLIPBOARD_SV.write();
282 if s.update_exts.len() > 20 {
283 s.update_exts.retain(|_, v| v.strong_count() > 0);
284 }
285 match s.update_exts.get(&data_type).and_then(|r| r.upgrade()) {
286 Some(r) => (*r).clone(),
287 None => {
288 if !VIEW_PROCESS.is_available() {
289 return Err(ClipboardError::Disconnected);
290 }
291 let r = match VIEW_PROCESS.clipboard()?.read_extension(data_type.clone())? {
292 Ok(r) => Ok(Some(r)),
293 Err(e) => match e {
294 ViewError::NotFound => Ok(None),
295 ViewError::NotSupported => Err(ClipboardError::NotSupported),
296 e => Err(ClipboardError::Other(e.to_txt())),
297 },
298 };
299
300 let arc = Arc::new(r.clone());
301 s.update_exts.insert(data_type, Arc::downgrade(&arc));
302 UPDATES.once_update("", || {
303 let _hold = arc;
304 });
305
306 r
307 }
308 }
309 }
310
311 pub fn set_extension(&self, data_type: impl Into<Txt>, data: IpcBytes) -> ResponseVar<Result<(), ClipboardError>> {
315 self.set_extension_impl(data_type.into(), data)
316 }
317 fn set_extension_impl(&self, data_type: Txt, data: IpcBytes) -> ResponseVar<Result<(), ClipboardError>> {
318 let (r, rsp) = response_var();
319 UPDATES.once_update("CLIPBOARD.set_extension", move || {
320 if !VIEW_PROCESS.is_available() {
321 return r.respond(Err(ClipboardError::Disconnected));
322 }
323 match VIEW_PROCESS.clipboard() {
324 Ok(c) => match c.write_extension(data_type, data) {
325 Ok(vr) => r.respond(vr.map_err(ClipboardError::from)),
326 Err(e) => r.respond(Err(e.into())),
327 },
328 Err(e) => r.respond(Err(e.into())),
329 }
330 });
331 rsp
332 }
333
334 pub fn available_types(&self) -> ClipboardTypes {
336 VIEW_PROCESS.info().clipboard.clone()
337 }
338}
339
340command! {
341 pub static CUT_CMD {
343 l10n!: true,
344 name: "Cut",
345 info: "Remove the selection and place it in the clipboard",
346 shortcut: [shortcut!(CTRL + 'X'), shortcut!(SHIFT + Delete), shortcut!(Cut)],
347 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
348 icon: wgt_fn!(|_| ICONS.get("cut")),
349 };
350
351 pub static COPY_CMD {
353 l10n!: true,
354 name: "Copy",
355 info: "Place a copy of the selection in the clipboard",
356 shortcut: [shortcut!(CTRL + 'C'), shortcut!(CTRL + Insert), shortcut!(Copy)],
357 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
358 icon: wgt_fn!(|_| ICONS.get("copy")),
359 };
360
361 pub static PASTE_CMD {
363 l10n!: true,
364 name: "Paste",
365 info: "Insert content from the clipboard",
366 shortcut: [shortcut!(CTRL + 'V'), shortcut!(SHIFT + Insert), shortcut!(Paste)],
367 shortcut_filter: ShortcutFilter::FOCUSED | ShortcutFilter::CMD_ENABLED,
368 icon: wgt_fn!(|_| ICONS.get("paste")),
369 };
370}