| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | use crate::commands; |
| 2 | 2 | use canvas::date::DateField; |
| 3 | 3 | use objc2::{ |
| 4 | | ClassType, DeclaredClass, |
| 4 | ClassType, DeclaredClass, class, |
| 5 | 5 | declare::ClassBuilder, |
| 6 | 6 | declare_class, msg_send, msg_send_id, mutability, |
| 7 | 7 | rc::{Allocated, Retained}, |
| ... | ... | @@ -176,36 +176,63 @@ pub fn traffic_lights(window: &Window) -> [[f32; 2]; 3] { |
| 176 | 176 | } |
| 177 | 177 | } |
| 178 | 178 | |
| 179 | | pub struct Clipboard(arboard::Clipboard); |
| 179 | /// The general pasteboard. |
| 180 | pub struct Clipboard(Retained<AnyObject>); |
| 180 | 181 | |
| 181 | 182 | impl Clipboard { |
| 182 | | pub fn new(_: &Window) -> Result<Self, arboard::Error> { |
| 183 | | arboard::Clipboard::new().map(Self) |
| 183 | pub fn new(_: &Window) -> Result<Self, &'static str> { |
| 184 | let board: Option<Retained<AnyObject>> = |
| 185 | unsafe { msg_send_id![class!(NSPasteboard), generalPasteboard] }; |
| 186 | board.map(Self).ok_or("No pasteboard is available") |
| 184 | 187 | } |
| 185 | 188 | |
| 186 | | pub fn set_text(&mut self, text: String) -> Result<(), arboard::Error> { |
| 187 | | self.0.set_text(text) |
| 189 | pub fn set_text(&mut self, text: String) -> Result<(), &'static str> { |
| 190 | let set: bool = unsafe { |
| 191 | let _: isize = msg_send![&self.0, clearContents]; |
| 192 | let (text, kind) = (NSString::from_str(&text), NSString::from_str(TEXT)); |
| 193 | msg_send![&self.0, setString: &*text, forType: &*kind] |
| 194 | }; |
| 195 | set.then_some(()).ok_or("The pasteboard refused the text") |
| 188 | 196 | } |
| 189 | 197 | |
| 190 | | pub fn get_text(&mut self) -> Result<String, arboard::Error> { |
| 191 | | self.0.get_text() |
| 198 | pub fn get_text(&mut self) -> Result<String, &'static str> { |
| 199 | self.string(TEXT).ok_or("The pasteboard holds no text") |
| 192 | 200 | } |
| 193 | 201 | |
| 202 | /// The files copied, as Finder puts them on the pasteboard. |
| 194 | 203 | pub fn get_files(&mut self) -> Vec<std::path::PathBuf> { |
| 195 | | self.0.get().file_list().unwrap_or_default() |
| 204 | objc2::rc::autoreleasepool(|_| unsafe { |
| 205 | let url: *const AnyObject = (class!(NSURL) as *const AnyClass).cast(); |
| 206 | let classes: Retained<AnyObject> = msg_send_id![class!(NSArray), arrayWithObject: url]; |
| 207 | let yes: Retained<AnyObject> = msg_send_id![class!(NSNumber), numberWithBool: true]; |
| 208 | let key = NSString::from_str("NSPasteboardURLReadingFileURLsOnlyKey"); |
| 209 | let options: Retained<AnyObject> = |
| 210 | msg_send_id![class!(NSDictionary), dictionaryWithObject: &*yes, forKey: &*key]; |
| 211 | let urls: Option<Retained<AnyObject>> = |
| 212 | msg_send_id![&self.0, readObjectsForClasses: &*classes, options: &*options]; |
| 213 | let Some(urls) = urls else { |
| 214 | return Vec::new(); |
| 215 | }; |
| 216 | let count: usize = msg_send![&urls, count]; |
| 217 | (0..count) |
| 218 | .filter_map(|index| { |
| 219 | let url: Retained<AnyObject> = msg_send_id![&urls, objectAtIndex: index]; |
| 220 | let path: Option<Retained<NSString>> = msg_send_id![&url, path]; |
| 221 | Some(path?.to_string().into()) |
| 222 | }) |
| 223 | .collect() |
| 224 | }) |
| 196 | 225 | } |
| 197 | 226 | |
| 198 | 227 | pub fn get_html(&mut self) -> Option<String> { |
| 199 | | self.0.get().html().ok() |
| 228 | self.string("public.html") |
| 200 | 229 | } |
| 201 | 230 | |
| 202 | 231 | /// The pasteboard's PNG as copied, keeping its resolution, or else its TIFF as a PNG. |
| 203 | 232 | pub fn get_picture(&mut self) -> Option<Vec<u8>> { |
| 204 | 233 | unsafe { |
| 205 | | let board: Retained<AnyObject> = |
| 206 | | msg_send_id![AnyClass::get("NSPasteboard")?, generalPasteboard]; |
| 207 | 234 | let data = |kind: &str| -> Option<Retained<AnyObject>> { |
| 208 | | msg_send_id![&board, dataForType: &*NSString::from_str(kind)] |
| 235 | msg_send_id![&self.0, dataForType: &*NSString::from_str(kind)] |
| 209 | 236 | }; |
| 210 | 237 | let png = match data("public.png") { |
| 211 | 238 | Some(png) => png, |
| ... | ... | @@ -214,8 +241,27 @@ impl Clipboard { |
| 214 | 241 | Some(ns_data_bytes(&png)) |
| 215 | 242 | } |
| 216 | 243 | } |
| 244 | |
| 245 | /// The first item's string of `kind`, where the pasteboard's own `stringForType:` joins |
| 246 | /// every item's. |
| 247 | fn string(&self, kind: &str) -> Option<String> { |
| 248 | objc2::rc::autoreleasepool(|_| unsafe { |
| 249 | let items: Option<Retained<AnyObject>> = msg_send_id![&self.0, pasteboardItems]; |
| 250 | let items = items?; |
| 251 | let count: usize = msg_send![&items, count]; |
| 252 | let kind = NSString::from_str(kind); |
| 253 | (0..count).find_map(|index| { |
| 254 | let item: Retained<AnyObject> = msg_send_id![&items, objectAtIndex: index]; |
| 255 | let string: Option<Retained<NSString>> = msg_send_id![&item, stringForType: &*kind]; |
| 256 | Some(string?.to_string()) |
| 257 | }) |
| 258 | }) |
| 259 | } |
| 217 | 260 | } |
| 218 | 261 | |
| 262 | /// NSPasteboardTypeString. |
| 263 | const TEXT: &str = "public.utf8-plain-text"; |
| 264 | |
| 219 | 265 | /// TIFF `data` as PNG data, keeping its resolution. |
| 220 | 266 | unsafe fn tiff_png(data: &AnyObject) -> Option<Retained<AnyObject>> { |
| 221 | 267 | const PNG: usize = 4; // NSBitmapImageFileTypePNG |
| ... | ... | @@ -1492,8 +1538,38 @@ pub fn input_language() -> String { |
| 1492 | 1538 | |
| 1493 | 1539 | #[cfg(test)] |
| 1494 | 1540 | mod tests { |
| 1541 | use super::*; |
| 1495 | 1542 | use crate::library::{Login, Mount}; |
| 1496 | 1543 | |
| 1544 | /// Text, a page and files read back as written, from a pasteboard of the test's own, |
| 1545 | /// never the user's. |
| 1546 | #[test] |
| 1547 | fn the_pasteboard_reads_back_text_pages_and_files() { |
| 1548 | let board: Retained<AnyObject> = |
| 1549 | unsafe { msg_send_id![class!(NSPasteboard), pasteboardWithUniqueName] }; |
| 1550 | let mut clipboard = Clipboard(board.clone()); |
| 1551 | clipboard.set_text("Notes \u{2713}".into()).unwrap(); |
| 1552 | assert_eq!(clipboard.get_text().unwrap(), "Notes \u{2713}"); |
| 1553 | assert_eq!(clipboard.get_html(), None); |
| 1554 | assert!(clipboard.get_files().is_empty()); |
| 1555 | unsafe { |
| 1556 | let _: isize = msg_send![&board, clearContents]; |
| 1557 | let html = NSString::from_str("<p>Page</p>"); |
| 1558 | let _: bool = |
| 1559 | msg_send![&board, setString: &*html, forType: &*NSString::from_str("public.html")]; |
| 1560 | } |
| 1561 | assert_eq!(clipboard.get_html().as_deref(), Some("<p>Page</p>")); |
| 1562 | let path = "/tmp/Notes and Plans/Plans.one"; |
| 1563 | unsafe { |
| 1564 | let url = objc2_foundation::NSURL::fileURLWithPath(&NSString::from_str(path)); |
| 1565 | let urls: Retained<AnyObject> = msg_send_id![class!(NSArray), arrayWithObject: &*url]; |
| 1566 | let _: isize = msg_send![&board, clearContents]; |
| 1567 | let _: bool = msg_send![&board, writeObjects: &*urls]; |
| 1568 | } |
| 1569 | assert_eq!(clipboard.get_files(), [std::path::PathBuf::from(path)]); |
| 1570 | unsafe { msg_send![&board, releaseGlobally] } |
| 1571 | } |
| 1572 | |
| 1497 | 1573 | #[link(name = "Security", kind = "framework")] |
| 1498 | 1574 | unsafe extern "C" { |
| 1499 | 1575 | fn SecKeychainCreate( |