Skip to content

Commit

Permalink
[mac] Adopt new save/open API
Browse files Browse the repository at this point in the history
This implements the new save/open (#1068) API on macOS.

The implementation here is slightly different; Cocoa uses the idea
of 'blocks' (a c extension for callbacks) in most of their async API,
so we don't need to to manually defer the work.
  • Loading branch information
cmyr committed Oct 19, 2020
1 parent c5df2cb commit c9a6deb
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 109 deletions.
1 change: 1 addition & 0 deletions druid-shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ features = ["d2d1_1", "dwrite", "winbase", "libloaderapi", "errhandlingapi", "wi
"d3d11", "dwmapi", "wincon", "fileapi", "processenv", "winbase", "handleapi"]

[target.'cfg(target_os="macos")'.dependencies]
block = "0.1.6"
cocoa = "0.23.0"
objc = "0.2.7"
core-graphics = "0.22.0"
Expand Down
192 changes: 95 additions & 97 deletions druid-shell/src/platform/mac/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,122 +25,120 @@ use objc::{class, msg_send, sel, sel_impl};
use super::util::{from_nsstring, make_nsstring};
use crate::dialog::{FileDialogOptions, FileDialogType};

pub(crate) type NSModalResponse = NSInteger;
const NSModalResponseOK: NSInteger = 1;
const NSModalResponseCancel: NSInteger = 0;

#[allow(clippy::cognitive_complexity)]
pub(crate) fn get_file_dialog_path(
ty: FileDialogType,
mut options: FileDialogOptions,
) -> Option<OsString> {
unsafe {
let panel: id = match ty {
FileDialogType::Open => msg_send![class!(NSOpenPanel), openPanel],
FileDialogType::Save => msg_send![class!(NSSavePanel), savePanel],
};

// Enable the user to choose whether file extensions are hidden in the dialog.
// This defaults to off, but is recommended to be turned on by Apple.
// https://developer.apple.com/design/human-interface-guidelines/macos/user-interaction/file-handling/
let () = msg_send![panel, setCanSelectHiddenExtension: YES];

// Set open dialog specific options
// NSOpenPanel inherits from NSSavePanel and thus has more options.
let mut set_type_filter = true;
if let FileDialogType::Open = ty {
if options.select_directories {
let () = msg_send![panel, setCanChooseDirectories: YES];
// Disable the selection of files in directory selection mode,
// because other platforms like Windows have no support for it,
// and expecting it to work will lead to buggy cross-platform behavior.
let () = msg_send![panel, setCanChooseFiles: NO];
// File filters are used by macOS to determine which paths are packages.
// If we are going to treat packages as directories, then file filters
// need to be disabled. Otherwise macOS will allow picking of regular files
// that match the filters as if they were directories too.
set_type_filter = !options.packages_as_directories;
}
if options.multi_selection {
let () = msg_send![panel, setAllowsMultipleSelection: YES];
}
pub(crate) unsafe fn get_path(panel: id, result: NSModalResponse) -> Option<OsString> {
match result {
NSModalResponseOK => {
let url: id = msg_send![panel, URL];
let path: id = msg_send![url, path];
let path: OsString = from_nsstring(path).into();
Some(path)
}
NSModalResponseCancel => None,
_ => unreachable!(),
}
}

// Set universal options
if options.packages_as_directories {
let () = msg_send![panel, setTreatsFilePackagesAsDirectories: YES];
#[allow(clippy::cognitive_complexity)]
pub(crate) unsafe fn build_panel(ty: FileDialogType, mut options: FileDialogOptions) -> id {
let panel: id = match ty {
FileDialogType::Open => msg_send![class!(NSOpenPanel), openPanel],
FileDialogType::Save => msg_send![class!(NSSavePanel), savePanel],
};

// Enable the user to choose whether file extensions are hidden in the dialog.
// This defaults to off, but is recommended to be turned on by Apple.
// https://developer.apple.com/design/human-interface-guidelines/macos/user-interaction/file-handling/
let () = msg_send![panel, setCanSelectHiddenExtension: YES];

// Set open dialog specific options
// NSOpenPanel inherits from NSSavePanel and thus has more options.
let mut set_type_filter = true;
if let FileDialogType::Open = ty {
if options.select_directories {
let () = msg_send![panel, setCanChooseDirectories: YES];
// Disable the selection of files in directory selection mode,
// because other platforms like Windows have no support for it,
// and expecting it to work will lead to buggy cross-platform behavior.
let () = msg_send![panel, setCanChooseFiles: NO];
// File filters are used by macOS to determine which paths are packages.
// If we are going to treat packages as directories, then file filters
// need to be disabled. Otherwise macOS will allow picking of regular files
// that match the filters as if they were directories too.
set_type_filter = !options.packages_as_directories;
}

if options.show_hidden {
let () = msg_send![panel, setShowsHiddenFiles: YES];
if options.multi_selection {
let () = msg_send![panel, setAllowsMultipleSelection: YES];
}
}

if let Some(default_name) = &options.default_name {
let () = msg_send![panel, setNameFieldStringValue: make_nsstring(default_name)];
}
// Set universal options
if options.packages_as_directories {
let () = msg_send![panel, setTreatsFilePackagesAsDirectories: YES];
}

if let Some(name_label) = &options.name_label {
let () = msg_send![panel, setNameFieldLabel: make_nsstring(name_label)];
}
if options.show_hidden {
let () = msg_send![panel, setShowsHiddenFiles: YES];
}

if let Some(title) = &options.title {
let () = msg_send![panel, setTitle: make_nsstring(title)];
}
if let Some(default_name) = &options.default_name {
let () = msg_send![panel, setNameFieldStringValue: make_nsstring(default_name)];
}

if let Some(text) = &options.button_text {
let () = msg_send![panel, setPrompt: make_nsstring(text)];
}
if let Some(name_label) = &options.name_label {
let () = msg_send![panel, setNameFieldLabel: make_nsstring(name_label)];
}

if let Some(path) = &options.starting_directory {
if let Some(path) = path.to_str() {
let url = NSURL::alloc(nil)
.initFileURLWithPath_isDirectory_(make_nsstring(path), YES)
.autorelease();
let () = msg_send![panel, setDirectoryURL: url];
}
if let Some(title) = &options.title {
let () = msg_send![panel, setTitle: make_nsstring(title)];
}

if let Some(text) = &options.button_text {
let () = msg_send![panel, setPrompt: make_nsstring(text)];
}

if let Some(path) = &options.starting_directory {
if let Some(path) = path.to_str() {
let url = NSURL::alloc(nil)
.initFileURLWithPath_isDirectory_(make_nsstring(path), YES)
.autorelease();
let () = msg_send![panel, setDirectoryURL: url];
}
}

if set_type_filter {
// If a default type was specified, then we must reorder the allowed types,
// because there's no way to specify the default type other than having it be first.
if let Some(dt) = &options.default_type {
let mut present = false;
if let Some(allowed_types) = options.allowed_types.as_mut() {
if let Some(idx) = allowed_types.iter().position(|t| t == dt) {
present = true;
allowed_types.swap(idx, 0);
}
}
if !present {
log::warn!("The default type {:?} is not present in allowed types.", dt);
if set_type_filter {
// If a default type was specified, then we must reorder the allowed types,
// because there's no way to specify the default type other than having it be first.
if let Some(dt) = &options.default_type {
let mut present = false;
if let Some(allowed_types) = options.allowed_types.as_mut() {
if let Some(idx) = allowed_types.iter().position(|t| t == dt) {
present = true;
allowed_types.swap(idx, 0);
}
}

// A vector of NSStrings. this must outlive `nsarray_allowed_types`.
let allowed_types = options.allowed_types.as_ref().map(|specs| {
specs
.iter()
.flat_map(|spec| spec.extensions.iter().map(|s| make_nsstring(s)))
.collect::<Vec<_>>()
});

let nsarray_allowed_types = allowed_types
.as_ref()
.map(|types| NSArray::arrayWithObjects(nil, types.as_slice()));
if let Some(nsarray) = nsarray_allowed_types {
let () = msg_send![panel, setAllowedFileTypes: nsarray];
if !present {
log::warn!("The default type {:?} is not present in allowed types.", dt);
}
}

let result: NSInteger = msg_send![panel, runModal];
match result {
NSModalResponseOK => {
let url: id = msg_send![panel, URL];
let path: id = msg_send![url, path];
let path: OsString = from_nsstring(path).into();
Some(path)
}
NSModalResponseCancel => None,
_ => unreachable!(),
// A vector of NSStrings. this must outlive `nsarray_allowed_types`.
let allowed_types = options.allowed_types.as_ref().map(|specs| {
specs
.iter()
.flat_map(|spec| spec.extensions.iter().map(|s| make_nsstring(s)))
.collect::<Vec<_>>()
});

let nsarray_allowed_types = allowed_types
.as_ref()
.map(|types| NSArray::arrayWithObjects(nil, types.as_slice()));
if let Some(nsarray) = nsarray_allowed_types {
let () = msg_send![panel, setAllowedFileTypes: nsarray];
}
}
panel
}
55 changes: 43 additions & 12 deletions druid-shell/src/platform/mac/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::mem;
use std::sync::{Arc, Mutex, Weak};
use std::time::Instant;

use block::ConcreteBlock;
use cocoa::appkit::{
CGFloat, NSApp, NSApplication, NSAutoresizingMaskOptions, NSBackingStoreBuffered, NSEvent,
NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowStyleMask,
Expand Down Expand Up @@ -933,26 +934,56 @@ impl WindowHandle {
}
}

pub fn open_file_sync(&mut self, options: FileDialogOptions) -> Option<FileInfo> {
dialog::get_file_dialog_path(FileDialogType::Open, options)
.map(|s| FileInfo { path: s.into() })
}

pub fn open_file(&mut self, _options: FileDialogOptions) -> Option<FileDialogToken> {
// TODO: implement this and get rid of open_file_sync
pub fn open_file_sync(&mut self, _options: FileDialogOptions) -> Option<FileInfo> {
log::warn!("open_file_sync should no longer be called on mac!");
None
}

pub fn save_as_sync(&mut self, options: FileDialogOptions) -> Option<FileInfo> {
dialog::get_file_dialog_path(FileDialogType::Save, options)
.map(|s| FileInfo { path: s.into() })
pub fn open_file(&mut self, options: FileDialogOptions) -> Option<FileDialogToken> {
let token = FileDialogToken::next();
let self_clone = self.clone();
unsafe {
let panel = dialog::build_panel(FileDialogType::Open, options);
let block = ConcreteBlock::new(move |response: dialog::NSModalResponse| {
let url = dialog::get_path(panel, response).map(|s| FileInfo { path: s.into() });
let view = self_clone.nsview.load();
if let Some(view) = (*view).as_ref() {
let view_state: *mut c_void = *view.get_ivar("viewState");
let view_state = &mut *(view_state as *mut ViewState);
(*view_state).handler.open_file(token, url);
}
});
let block = block.copy();
let () = msg_send![panel, beginWithCompletionHandler: block];
}
Some(token)
}

pub fn save_as(&mut self, _options: FileDialogOptions) -> Option<FileDialogToken> {
// TODO: implement this and get rid of save_as_sync
pub fn save_as_sync(&mut self, _options: FileDialogOptions) -> Option<FileInfo> {
log::warn!("save_as_sync should no longer be called on mac!");
None
}

pub fn save_as(&mut self, options: FileDialogOptions) -> Option<FileDialogToken> {
let token = FileDialogToken::next();
let self_clone = self.clone();
unsafe {
let panel = dialog::build_panel(FileDialogType::Save, options);
let block = ConcreteBlock::new(move |response: dialog::NSModalResponse| {
let url = dialog::get_path(panel, response).map(|s| FileInfo { path: s.into() });
let view = self_clone.nsview.load();
if let Some(view) = (*view).as_ref() {
let view_state: *mut c_void = *view.get_ivar("viewState");
let view_state = &mut *(view_state as *mut ViewState);
(*view_state).handler.save_as(token, url);
}
});
let block = block.copy();
let () = msg_send![panel, beginWithCompletionHandler: block];
}
Some(token)
}

/// Set the title for this menu.
pub fn set_title(&self, title: &str) {
unsafe {
Expand Down

0 comments on commit c9a6deb

Please sign in to comment.