From 80ba209413a71eb302791630052382a63ac5ba3e Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Tue, 10 Mar 2020 20:09:24 -0700 Subject: [PATCH] Ongoing efforts, experimenting with stack/heap issues (ObjC/NSObject/AppKit are... mostly heap already), further work on Pasteboard support --- appkit/Cargo.toml | 1 + appkit/src/dragdrop.rs | 13 ++++- appkit/src/error.rs | 51 ++++++++++++++++ appkit/src/filesystem/manager.rs | 91 ++++++++++++++++++----------- appkit/src/lib.rs | 6 +- appkit/src/pasteboard/pasteboard.rs | 67 ++++++++++++++++++++- appkit/src/pasteboard/types.rs | 19 ++++-- appkit/src/view/class.rs | 39 ++++++++++--- appkit/src/view/controller.rs | 2 +- appkit/src/view/traits.rs | 21 +++++-- appkit/src/view/view.rs | 8 ++- appkit/src/window/config.rs | 2 +- derives/src/lib.rs | 25 +++++++- 13 files changed, 283 insertions(+), 62 deletions(-) create mode 100644 appkit/src/error.rs diff --git a/appkit/Cargo.toml b/appkit/Cargo.toml index 214b9ec..33bd6c6 100644 --- a/appkit/Cargo.toml +++ b/appkit/Cargo.toml @@ -16,6 +16,7 @@ lazy_static = "1" objc = "0.2.7" objc_id = "0.1.1" uuid = { version = "0.8", features = ["v4"] } +url = "2.1.1" [features] enable-webview-downloading = [] diff --git a/appkit/src/dragdrop.rs b/appkit/src/dragdrop.rs index ef02472..5fed05c 100644 --- a/appkit/src/dragdrop.rs +++ b/appkit/src/dragdrop.rs @@ -5,8 +5,11 @@ use cocoa::foundation::NSUInteger; use objc::runtime::Object; +use objc::{msg_send, sel, sel_impl}; use objc_id::Id; +use crate::pasteboard::Pasteboard; + /// Represents operations that can happen for a given drag/drop scenario. pub enum DragOperation { /// No drag operations are allowed. @@ -56,5 +59,13 @@ pub struct DragInfo { } impl DragInfo { - + /// Returns a wrapped Pasteboard instance, enabling you to get the contents of whatever is + /// being pasted/dragged/dropped/etc. + /// + /// Note: in general, you should not store pasteboards. + pub fn get_pasteboard(&self) -> Pasteboard { + unsafe { + Pasteboard::with(msg_send![&*self.info, draggingPasteboard]) + } + } } diff --git a/appkit/src/error.rs b/appkit/src/error.rs new file mode 100644 index 0000000..d895931 --- /dev/null +++ b/appkit/src/error.rs @@ -0,0 +1,51 @@ +//! A wrapper for `NSError`, which can be (and is) bubbled up for certain calls in this library. It +//! attempts to be thread safe where possible, and extract the "default" usable information out of +//! an `NSError`. This might not be what you need, though, so if it's missing something... well, +//! it's up for discussion. + +use std::error; +use std::fmt; + +use cocoa::base::id; +use objc::{msg_send, sel, sel_impl}; + +use crate::utils::str_from; + +/// A wrapper around pieces of data extracted from `NSError`. This could be improved: right now, it +/// allocates `String` instances when theoretically it could be avoided, and we might be erasing +/// certain parts of the `NSError` object that are useful. +#[derive(Clone, Debug)] +pub struct AppKitError { + pub code: usize, + pub domain: String, + pub description: String +} + +impl AppKitError { + /// Given an `NSError` (i.e, an id reference) we'll pull out the relevant information and + /// configure this. We pull out the information as it makes the error thread safe this way, + /// which is... easier, in some cases. + pub fn new(error: id) -> Box { + let (code, domain, description) = unsafe { + let code: usize = msg_send![error, code]; + let domain: id = msg_send![error, domain]; + let description: id = msg_send![error, localizedDescription]; + + (code, domain, description) + }; + + Box::new(AppKitError { + code: code, + domain: str_from(domain).to_string(), + description: str_from(description).to_string() + }) + } +} + +impl fmt::Display for AppKitError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.description) + } +} + +impl error::Error for AppKitError {} diff --git a/appkit/src/filesystem/manager.rs b/appkit/src/filesystem/manager.rs index e29d0d4..8f50f12 100644 --- a/appkit/src/filesystem/manager.rs +++ b/appkit/src/filesystem/manager.rs @@ -1,69 +1,92 @@ //! A wrapper for `NSFileManager`, which is necessary for macOS/iOS (the sandbox makes things //! tricky, and this transparently handles it for you). -use std::rc::Rc; -use std::cell::RefCell; +use std::error::Error; +use std::sync::RwLock; use cocoa::base::{id, nil, NO}; -use cocoa::foundation::NSUInteger; - +use cocoa::foundation::{NSString, NSUInteger}; use objc_id::Id; -use objc::runtime::Object; +use objc::runtime::{BOOL, Object}; use objc::{class, msg_send, sel, sel_impl}; +use url::Url; +use crate::error::AppKitError; use crate::filesystem::enums::{SearchPathDirectory, SearchPathDomainMask}; use crate::utils::str_from; -pub struct FileManagerInner { - pub manager: Id +pub struct FileManager { + pub manager: RwLock> } -impl Default for FileManagerInner { +impl Default for FileManager { + /// Returns a default file manager, which maps to the default system file manager. For common + /// and simple tasks, with no callbacks, you might want this. fn default() -> Self { - FileManagerInner { - manager: unsafe { + FileManager { + manager: RwLock::new(unsafe { let manager: id = msg_send![class!(NSFileManager), defaultManager]; Id::from_ptr(manager) - } + }) } } } -impl FileManagerInner { - pub fn get_path(&self, directory: SearchPathDirectory, in_domain: SearchPathDomainMask) -> Result> { +impl FileManager { + /// Returns a new FileManager that opts in to delegate methods. + pub fn new() -> Self { + FileManager { + manager: RwLock::new(unsafe { + let manager: id = msg_send![class!(NSFileManager), new]; + Id::from_ptr(manager) + }) + } + } + + /// Given a directory/domain combination, will attempt to get the directory that matches. + /// Returns a PathBuf that wraps the given location. If there's an error on the Objective-C + /// side, we attempt to catch it and bubble it up. + pub fn get_directory(&self, directory: SearchPathDirectory, in_domain: SearchPathDomainMask) -> Result> { let dir: NSUInteger = directory.into(); let mask: NSUInteger = in_domain.into(); - unsafe { - let dir: id = msg_send![&*self.manager, URLForDirectory:dir + let directory = unsafe { + let manager = self.manager.read().unwrap(); + let dir: id = msg_send![&**manager, URLForDirectory:dir inDomain:mask appropriateForURL:nil create:NO error:nil]; - let s: id = msg_send![dir, path]; - Ok(str_from(s).to_string()) - } + let s: id = msg_send![dir, absoluteString]; + str_from(s) + }; + + Url::parse(directory).map_err(|e| e.into()) } -} -#[derive(Default)] -pub struct FileManager(Rc>); + /// Given two paths, moves file (`from`) to the location specified in `to`. This can result in + /// an error on the Objective-C side, which we attempt to handle and bubble up as a result if + /// so. + pub fn move_item(&self, from: Url, to: Url) -> Result<(), Box> { + unsafe { + let s = NSString::alloc(nil).init_str(from.as_str()); + let from_url: id = msg_send![class!(NSURL), URLWithString:s]; -impl FileManager { - pub fn new() -> Self { - FileManager(Rc::new(RefCell::new(FileManagerInner { - manager: unsafe { - let manager: id = msg_send![class!(NSFileManager), new]; - Id::from_ptr(manager) + let s2 = NSString::alloc(nil).init_str(to.as_str()); + let to_url: id = msg_send![class!(NSURL), URLWithString:s2]; + + // This should potentially be write(), but the backing class handles this logic + // already, so... going to leave it as read. + let manager = self.manager.read().unwrap(); + + let error: id = nil; + let result: BOOL = msg_send![&**manager, moveItemAtURL:from_url toURL:to_url error:&error]; + if result == NO { + return Err(AppKitError::new(error)); } - }))) - } + } - pub fn get_path(&self, directory: SearchPathDirectory, in_domain: SearchPathDomainMask) -> Result> { - let manager = self.0.borrow(); - manager.get_path(directory, in_domain) + Ok(()) } - - //pub fn contents_of(directory: &str, properties: &[ } diff --git a/appkit/src/lib.rs b/appkit/src/lib.rs index 796ed45..c60a1c1 100644 --- a/appkit/src/lib.rs +++ b/appkit/src/lib.rs @@ -27,6 +27,7 @@ pub mod color; pub mod collection_view; pub mod constants; pub mod dragdrop; +pub mod error; pub mod events; pub mod filesystem; pub mod geometry; @@ -40,6 +41,9 @@ pub mod view; pub mod webview; pub mod window; +// We re-export these so that they can be used without increasing build times. +pub use url; + pub mod prelude { pub use crate::app::{App, AppDelegate}; @@ -60,6 +64,6 @@ pub mod prelude { pub use crate::view::{View, ViewController, ViewWrapper}; pub use appkit_derive::{ - WindowWrapper + WindowWrapper, ViewWrapper }; } diff --git a/appkit/src/pasteboard/pasteboard.rs b/appkit/src/pasteboard/pasteboard.rs index c4fff46..7e5f315 100644 --- a/appkit/src/pasteboard/pasteboard.rs +++ b/appkit/src/pasteboard/pasteboard.rs @@ -2,13 +2,18 @@ //! (think: drag and drop between applications). It exposes a Rust interface that tries to be //! complete, but might not cover everything 100% right now - feel free to pull request. -use cocoa::base::id; +use std::error::Error; +use cocoa::base::{id, nil}; +use cocoa::foundation::{NSArray}; use objc::runtime::Object; use objc::{class, msg_send, sel, sel_impl}; use objc_id::Id; +use url::Url; -use crate::pasteboard::types::PasteboardName; +use crate::error::AppKitError; +use crate::pasteboard::types::{PasteboardName, PasteboardType}; +use crate::utils::str_from; /// Represents an `NSPasteboard`, enabling you to handle copy/paste/drag and drop. pub struct Pasteboard { @@ -32,7 +37,7 @@ impl Pasteboard { } } - /// Should be pasteboardname enum! + /// Retrieves the system Pasteboard for the given name/type. pub fn named(name: PasteboardName) -> Self { Pasteboard { inner: unsafe { @@ -64,4 +69,60 @@ impl Pasteboard { let _: () = msg_send![&*self.inner, clearContents]; } } + + /// Looks inside the pasteboard contents and extracts what FileURLs are there, if any. + pub fn get_file_urls(&self) -> Result, Box> { + unsafe { + let mut i = 0; + + let class: id = msg_send![class!(NSURL), class]; + let classes: id = NSArray::arrayWithObjects(nil, &[class]); + let contents: id = msg_send![&*self.inner, readObjectsForClasses:classes options:nil]; + + // This can happen if the Pasteboard server has an error in returning items. + // In our case, we'll bubble up an error by checking the pasteboard. + if contents == nil { + // This error is not necessarily "correct", but in the event of an error in + // Pasteboard server retrieval I'm not sure where to check... and this stuff is + // kinda ancient and has conflicting docs in places. ;P + return Err(Box::new(AppKitError { + code: 666, + domain: "com.appkit-rs.pasteboard".to_string(), + description: "Pasteboard server returned no data.".to_string() + })); + } + + let count: usize = msg_send![contents, count]; + let mut urls: Vec = Vec::with_capacity(count); + + loop { + let nsurl: id = msg_send![contents, objectAtIndex:i]; + let path: id = msg_send![nsurl, path]; + let s = str_from(path); + urls.push(Url::parse(&format!("file://{}", s))?); + + i += 1; + if i == count { break; } + } + + Ok(urls) + } + } +/* + /// Retrieves the pasteboard contents as a string. This can be `None` (`nil` on the Objective-C + /// side) if the pasteboard data doesn't match the requested type, so check accordingly. + /// + /// Note: In macOS 10.6 and later, if the receiver contains multiple items that can provide string, + /// RTF, or RTFD data, the text data from each item is returned as a combined result separated by newlines. + /// This Rust wrapper is a quick pass, and could be improved. ;P + pub fn contents_for(&self, pasteboard_type: PasteboardType) -> Option { + unsafe { + let contents: id = msg_send![&*self.inner, stringForType:pasteboard_type.to_nsstring()]; + if contents != nil { + return Some(str_from(contents).to_string()); + } + } + + None + }*/ } diff --git a/appkit/src/pasteboard/types.rs b/appkit/src/pasteboard/types.rs index ce0ed9f..74abc0c 100644 --- a/appkit/src/pasteboard/types.rs +++ b/appkit/src/pasteboard/types.rs @@ -7,10 +7,19 @@ use cocoa::foundation::NSString; /// Constants for the standard system pasteboard names. #[derive(Debug, Copy, Clone)] pub enum PasteboardName { + /// The dragging/dropping pasteboard. Drag, + + /// The find pasteboard. Find, + + /// The font pasteboard. Font, + + /// The general pasteboard. General, + + /// The ruler pasteboard. Ruler } @@ -19,11 +28,11 @@ impl PasteboardName { pub fn to_nsstring(&self) -> id { unsafe { NSString::alloc(nil).init_str(match self { - PasteboardName::Drag => "", - PasteboardName::Find => "", - PasteboardName::Font => "", - PasteboardName::General => "", - PasteboardName::Ruler => "" + PasteboardName::Drag => "Apple CFPasteboard drag", + PasteboardName::Find => "Apple CFPasteboard find", + PasteboardName::Font => "Apple CFPasteboard font", + PasteboardName::General => "Apple CFPasteboard general", + PasteboardName::Ruler => "Apple CFPasteboard ruler" }) } } diff --git a/appkit/src/view/class.rs b/appkit/src/view/class.rs index 43ab124..b8625ae 100644 --- a/appkit/src/view/class.rs +++ b/appkit/src/view/class.rs @@ -15,8 +15,10 @@ use cocoa::foundation::{NSUInteger}; use objc::declare::ClassDecl; use objc::runtime::{Class, Object, Sel, BOOL}; use objc::{msg_send, sel, sel_impl}; +use objc_id::Id; use crate::constants::{BACKGROUND_COLOR, VIEW_CONTROLLER_PTR}; +use crate::dragdrop::DragInfo; use crate::view::traits::ViewController; /// Enforces normalcy, or: a needlessly cruel method in terms of the name. You get the idea though. @@ -37,21 +39,25 @@ extern fn update_layer(this: &Object, _: Sel) { } /// Called when a drag/drop operation has entered this view. -extern fn dragging_entered(this: &mut Object, _: Sel, _: id) -> NSUInteger { +extern fn dragging_entered(this: &mut Object, _: Sel, info: id) -> NSUInteger { unsafe { let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let view = ptr as *const T; - (*view).dragging_entered().into() + (*view).dragging_entered(DragInfo { + info: Id::from_ptr(info) + }).into() } } /// Called when a drag/drop operation has entered this view. -extern fn prepare_for_drag_operation(this: &mut Object, _: Sel, _: id) -> BOOL { +extern fn prepare_for_drag_operation(this: &mut Object, _: Sel, info: id) -> BOOL { unsafe { let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let view = ptr as *const T; - match (*view).prepare_for_drag_operation() { + match (*view).prepare_for_drag_operation(DragInfo { + info: Id::from_ptr(info) + }) { true => YES, false => NO } @@ -59,12 +65,14 @@ extern fn prepare_for_drag_operation(this: &mut Object, _: Se } /// Called when a drag/drop operation has entered this view. -extern fn perform_drag_operation(this: &mut Object, _: Sel, _: id) -> BOOL { +extern fn perform_drag_operation(this: &mut Object, _: Sel, info: id) -> BOOL { unsafe { let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let view = ptr as *const T; - match (*view).perform_drag_operation() { + match (*view).perform_drag_operation(DragInfo { + info: Id::from_ptr(info) + }) { true => YES, false => NO } @@ -72,11 +80,25 @@ extern fn perform_drag_operation(this: &mut Object, _: Sel, _ } /// Called when a drag/drop operation has entered this view. -extern fn dragging_exited(this: &mut Object, _: Sel, _: id) { +extern fn conclude_drag_operation(this: &mut Object, _: Sel, info: id) { unsafe { let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); let view = ptr as *const T; - (*view).dragging_exited(); + + (*view).conclude_drag_operation(DragInfo { + info: Id::from_ptr(info) + }); + } +} + +/// Called when a drag/drop operation has entered this view. +extern fn dragging_exited(this: &mut Object, _: Sel, info: id) { + unsafe { + let ptr: usize = *this.get_ivar(VIEW_CONTROLLER_PTR); + let view = ptr as *const T; + (*view).dragging_exited(DragInfo { + info: Id::from_ptr(info) + }); } } @@ -103,6 +125,7 @@ pub(crate) fn register_view_class() -> *const Class { decl.add_method(sel!(draggingEntered:), dragging_entered:: as extern fn (&mut Object, _, _) -> NSUInteger); decl.add_method(sel!(prepareForDragOperation:), prepare_for_drag_operation:: as extern fn (&mut Object, _, _) -> BOOL); decl.add_method(sel!(performDragOperation:), perform_drag_operation:: as extern fn (&mut Object, _, _) -> BOOL); + decl.add_method(sel!(concludeDragOperation:), conclude_drag_operation:: as extern fn (&mut Object, _, _)); decl.add_method(sel!(draggingExited:), dragging_exited:: as extern fn (&mut Object, _, _)); VIEW_CLASS = decl.register(); diff --git a/appkit/src/view/controller.rs b/appkit/src/view/controller.rs index 083fa0f..9312672 100644 --- a/appkit/src/view/controller.rs +++ b/appkit/src/view/controller.rs @@ -3,7 +3,7 @@ use std::sync::Once; -use cocoa::base::{id, nil, YES, NO}; +use cocoa::base::{id, NO}; use cocoa::foundation::{NSRect}; use objc::declare::ClassDecl; diff --git a/appkit/src/view/traits.rs b/appkit/src/view/traits.rs index ec456c7..904d673 100644 --- a/appkit/src/view/traits.rs +++ b/appkit/src/view/traits.rs @@ -4,7 +4,7 @@ use objc::runtime::Object; use objc_id::ShareId; -use crate::dragdrop::DragOperation; +use crate::dragdrop::{DragInfo, DragOperation}; pub trait ViewWrapper { fn get_handle(&self) -> Option>; @@ -13,8 +13,19 @@ pub trait ViewWrapper { pub trait ViewController { fn did_load(&self); - fn dragging_entered(&self) -> DragOperation { DragOperation::None } - fn prepare_for_drag_operation(&self) -> bool { false } - fn perform_drag_operation(&self) -> bool { false } - fn dragging_exited(&self) {} + /// Invoked when the dragged image enters destination bounds or frame; returns dragging operation to perform. + fn dragging_entered(&self, _info: DragInfo) -> DragOperation { DragOperation::None } + + /// Invoked when the image is released, allowing the receiver to agree to or refuse drag operation. + fn prepare_for_drag_operation(&self, _info: DragInfo) -> bool { false } + + /// Invoked after the released image has been removed from the screen, signaling the receiver to import the pasteboard data. + fn perform_drag_operation(&self, _info: DragInfo) -> bool { false } + + /// Invoked when the dragging operation is complete, signaling the receiver to perform any necessary clean-up. + fn conclude_drag_operation(&self, _info: DragInfo) {} + + /// Invoked when the dragged image exits the destination’s bounds rectangle (in the case of a view) or its frame + /// rectangle (in the case of a window object). + fn dragging_exited(&self, _info: DragInfo) {} } diff --git a/appkit/src/view/view.rs b/appkit/src/view/view.rs index 2d4942f..e587918 100644 --- a/appkit/src/view/view.rs +++ b/appkit/src/view/view.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use std::cell::RefCell; -use cocoa::base::{id, nil, YES, NO}; +use cocoa::base::{id, nil, YES}; use cocoa::foundation::NSArray; use objc_id::ShareId; @@ -86,3 +86,9 @@ impl View { view.set_background_color(color); } } + +impl std::fmt::Debug for View { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "View ({:p})", self) + } +} diff --git a/appkit/src/window/config.rs b/appkit/src/window/config.rs index 15d2808..3e114ce 100644 --- a/appkit/src/window/config.rs +++ b/appkit/src/window/config.rs @@ -35,7 +35,7 @@ impl Default for WindowConfig { let dimensions = NSRect::new(NSPoint::new(0., 0.), NSSize::new(800., 600.)); let style = WindowStyle::Resizable | WindowStyle::Miniaturizable | WindowStyle::UnifiedTitleAndToolbar | - WindowStyle::Closable | WindowStyle::Titled; + WindowStyle::Closable | WindowStyle::Titled | WindowStyle::FullSizeContentView; let alloc: id = msg_send![class!(NSWindow), alloc]; let window: id = msg_send![alloc, initWithContentRect:dimensions styleMask:style backing:2 as NSUInteger defer:YES]; diff --git a/derives/src/lib.rs b/derives/src/lib.rs index 8c548fd..3c7e9de 100644 --- a/derives/src/lib.rs +++ b/derives/src/lib.rs @@ -7,13 +7,13 @@ use crate::proc_macro::TokenStream; use quote::quote; use syn::{DeriveInput, parse_macro_input}; -/// Derivces an `appkit::prelude::WinWrapper` block, which implements forwarding methods for things +/// Derivces an `appkit::prelude::WindowWrapper` block, which implements forwarding methods for things /// like setting the window title, or showing and closing it. It currently expects that the wrapped /// struct has `window` as the field holding the `Window` from `appkit-rs`. /// /// Note that this expects that pointers to Window(s) should not move once created. #[proc_macro_derive(WindowWrapper)] -pub fn impl_window_controller(input: TokenStream) -> TokenStream { +pub fn impl_window_wrapper(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; @@ -30,3 +30,24 @@ pub fn impl_window_controller(input: TokenStream) -> TokenStream { TokenStream::from(expanded) } + +/// Derives an `appkit::prelude::ViewWrapper` block, which implements some necessary bits and +/// pieces for View handling. +#[proc_macro_derive(ViewWrapper)] +pub fn impl_view_wrapper(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = &input.ident; + let generics = input.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let expanded = quote! { + impl #impl_generics appkit::prelude::ViewWrapper for #name #ty_generics #where_clause { + fn get_handle(&self) -> Option> { + self.view.get_handle() + } + } + }; + + TokenStream::from(expanded) +}