| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | use crate::commands; | 1 | use crate::commands; |
| 2 | use canvas::date::DateField; | 2 | use canvas::date::DateField; |
| 3 | use objc2::{ | 3 | use objc2::{ |
| 4 | ClassType, DeclaredClass, | 4 | ClassType, DeclaredClass, class, |
| 5 | declare::ClassBuilder, | 5 | declare::ClassBuilder, |
| 6 | declare_class, msg_send, msg_send_id, mutability, | 6 | declare_class, msg_send, msg_send_id, mutability, |
| 7 | rc::{Allocated, Retained}, | 7 | rc::{Allocated, Retained}, |
| ... | @@ -176,36 +176,63 @@ pub fn traffic_lights(window: &Window) -> [[f32; 2]; 3] { | ... | @@ -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 | impl Clipboard { | 182 | impl Clipboard { |
| 182 | pub fn new(_: &Window) -> Result<Self, arboard::Error> { | 183 | pub fn new(_: &Window) -> Result<Self, &'static str> { |
| 183 | arboard::Clipboard::new().map(Self) | 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> { | 189 | pub fn set_text(&mut self, text: String) -> Result<(), &'static str> { |
| 187 | self.0.set_text(text) | 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> { | 198 | pub fn get_text(&mut self) -> Result<String, &'static str> { |
| 191 | self.0.get_text() | 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 | pub fn get_files(&mut self) -> Vec<std::path::PathBuf> { | 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 | pub fn get_html(&mut self) -> Option<String> { | 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 | /// The pasteboard's PNG as copied, keeping its resolution, or else its TIFF as a PNG. | 231 | /// The pasteboard's PNG as copied, keeping its resolution, or else its TIFF as a PNG. |
| 203 | pub fn get_picture(&mut self) -> Option<Vec<u8>> { | 232 | pub fn get_picture(&mut self) -> Option<Vec<u8>> { |
| 204 | unsafe { | 233 | unsafe { |
| 205 | let board: Retained<AnyObject> = | | |
| 206 | msg_send_id![AnyClass::get("NSPasteboard")?, generalPasteboard]; | | |
| 207 | let data = |kind: &str| -> Option<Retained<AnyObject>> { | 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 | let png = match data("public.png") { | 237 | let png = match data("public.png") { |
| 211 | Some(png) => png, | 238 | Some(png) => png, |
| ... | @@ -214,8 +241,27 @@ impl Clipboard { | ... | @@ -214,8 +241,27 @@ impl Clipboard { |
| 214 | Some(ns_data_bytes(&png)) | 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 | /// TIFF `data` as PNG data, keeping its resolution. | 265 | /// TIFF `data` as PNG data, keeping its resolution. |
| 220 | unsafe fn tiff_png(data: &AnyObject) -> Option<Retained<AnyObject>> { | 266 | unsafe fn tiff_png(data: &AnyObject) -> Option<Retained<AnyObject>> { |
| 221 | const PNG: usize = 4; // NSBitmapImageFileTypePNG | 267 | const PNG: usize = 4; // NSBitmapImageFileTypePNG |
| ... | @@ -1492,8 +1538,38 @@ pub fn input_language() -> String { | ... | @@ -1492,8 +1538,38 @@ pub fn input_language() -> String { |
| 1492 | | 1538 | |
| 1493 | #[cfg(test)] | 1539 | #[cfg(test)] |
| 1494 | mod tests { | 1540 | mod tests { |
| | 1541 | use super::*; |
| 1495 | use crate::library::{Login, Mount}; | 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 | #[link(name = "Security", kind = "framework")] | 1573 | #[link(name = "Security", kind = "framework")] |
| 1498 | unsafe extern "C" { | 1574 | unsafe extern "C" { |
| 1499 | fn SecKeychainCreate( | 1575 | fn SecKeychainCreate( |