Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add file format popup mac #1847

Merged
merged 8 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions druid-shell/examples/shello.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ impl WinHandler for HelloState {
]);
self.handle.open_file(options);
}
0x102 => {
let options = FileDialogOptions::new().show_hidden().allowed_types(vec![
FileSpec::new("Rust Files", &["rs", "toml"]),
FileSpec::TEXT,
FileSpec::JPG,
]);
self.handle.save_as(options);
}
_ => println!("unexpected id {}", id),
}
}
Expand All @@ -66,6 +74,10 @@ impl WinHandler for HelloState {
println!("open file result: {:?}", file_info);
}

fn save_as(&mut self, _token: FileDialogToken, file: Option<FileInfo>) {
println!("save file result: {:?}", file);
}

fn key_down(&mut self, event: KeyEvent) -> bool {
println!("keydown: {:?}", event);
false
Expand Down Expand Up @@ -138,6 +150,13 @@ fn main() {
true,
false,
);
file_menu.add_item(
0x102,
"S&ave",
Some(&HotKey::new(SysMods::Cmd, "s")),
true,
false,
);
let mut menubar = Menu::new();
menubar.add_dropdown(Menu::new(), "Application", true);
menubar.add_dropdown(file_menu, "&File", true);
Expand Down
10 changes: 8 additions & 2 deletions druid-shell/src/backend/gtk/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,10 @@ impl WindowState {
options,
)
.ok()
.map(|s| FileInfo { path: s.into() });
.map(|s| FileInfo {
path: s.into(),
format: None,
});
self.with_handler(|h| h.open_file(token, file_info));
}
DeferredOp::SaveAs(options, token) => {
Expand All @@ -852,7 +855,10 @@ impl WindowState {
options,
)
.ok()
.map(|s| FileInfo { path: s.into() });
.map(|s| FileInfo {
path: s.into(),
format: None,
});
self.with_handler(|h| h.save_as(token, file_info));
}
DeferredOp::ContextMenu(menu, handle) => {
Expand Down
141 changes: 138 additions & 3 deletions druid-shell/src/backend/mac/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,36 @@

use std::ffi::OsString;

use cocoa::appkit::NSView;
use cocoa::base::{id, nil, NO, YES};
use cocoa::foundation::{NSArray, NSAutoreleasePool, NSInteger, NSURL};
use cocoa::foundation::{
NSArray, NSAutoreleasePool, NSInteger, NSPoint, NSRect, NSSize, NSString, NSURL,
};
use objc::{class, msg_send, sel, sel_impl};

use super::util::{from_nsstring, make_nsstring};
use crate::dialog::{FileDialogOptions, FileDialogType};
use crate::{FileInfo, FileSpec};

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

pub(crate) unsafe fn get_path(panel: id, result: NSModalResponse) -> Option<OsString> {
pub(crate) unsafe fn get_file_info(
panel: id,
options: FileDialogOptions,
result: NSModalResponse,
) -> Option<FileInfo> {
match result {
NSModalResponseOK => {
let url: id = msg_send![panel, URL];
let path: id = msg_send![url, path];
let (path, format) = rewritten_path(panel, path, options);
let path: OsString = from_nsstring(path).into();
Some(path)
Some(FileInfo {
path: path.into(),
format,
})
}
NSModalResponseCancel => None,
_ => unreachable!(),
Expand Down Expand Up @@ -109,6 +121,16 @@ pub(crate) unsafe fn build_panel(ty: FileDialogType, mut options: FileDialogOpti
}
}

// If we have non-empty allowed types and a file save dialog,
// we add a accessory view to set the file format
match (&options.allowed_types, ty) {
(Some(allowed_types), FileDialogType::Save) if !allowed_types.is_empty() => {
let accessory_view = allowed_types_accessory_view(allowed_types);
let _: () = msg_send![panel, setAccessoryView: accessory_view];
}
_ => (),
}

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.
Expand Down Expand Up @@ -142,3 +164,116 @@ pub(crate) unsafe fn build_panel(ty: FileDialogType, mut options: FileDialogOpti
}
panel
}

// AppKit has a built-in file format accessory view. However, this is only
// displayed for `NSDocument` based apps. We have to construct our own `NSView`
// hierachy to implement something similar.
unsafe fn allowed_types_accessory_view(allowed_types: &[crate::FileSpec]) -> id {
// Build the View Structure required to have file format popup.
// This requires a container view, a label, a popup button,
// and some layout code to make sure it behaves correctly for
// fixed size and resizable views
let total_frame = NSRect::new(
NSPoint { x: 0.0, y: 0.0 },
NSSize {
width: 320.0,
height: 30.0,
},
);

let padding = 10.0;

// Prepare the label
let (label, label_size) = file_format_label();

// Place the label centered (similar to the popup button)
label.setFrameOrigin(NSPoint {
x: padding,
y: label_size.height / 2.0,
});

// Prepare the popup button
let popup_frame = NSRect::new(
NSPoint {
x: padding + label_size.width + padding,
y: 0.0,
},
NSSize {
width: total_frame.size.width - (padding * 3.0) - label_size.width,
height: total_frame.size.height,
},
);

let popup_button = file_format_popup_button(allowed_types, popup_frame);

// Prepare the container
let container_view: id = msg_send![class!(NSView), alloc];
let container_view: id = container_view.initWithFrame_(total_frame);

container_view.addSubview_(label);
container_view.addSubview_(popup_button);

container_view.autorelease()
}

const FileFormatPopoverTag: NSInteger = 10;

unsafe fn file_format_popup_button(allowed_types: &[crate::FileSpec], popup_frame: NSRect) -> id {
let popup_button: id = msg_send![class!(NSPopUpButton), alloc];
let _: () = msg_send![popup_button, initWithFrame:popup_frame pullsDown:false];
for allowed_type in allowed_types {
let title = NSString::alloc(nil)
.init_str(allowed_type.name)
.autorelease();
msg_send![popup_button, addItemWithTitle: title]
}
let _: () = msg_send![popup_button, setTag: FileFormatPopoverTag];
popup_button.autorelease()
}

unsafe fn file_format_label() -> (id, NSSize) {
let label: id = msg_send![class!(NSTextField), new];
let _: () = msg_send![label, setBezeled:false];
let _: () = msg_send![label, setDrawsBackground:false];
// FIXME: As we have to roll our own view hierachy, we're not getting a translated
// title here. So we ought to find a way to translate this.
let title = NSString::alloc(nil).init_str("File Format:").autorelease();
let _: () = msg_send![label, setStringValue: title];
let _: () = msg_send![label, sizeToFit];
(label.autorelease(), label.frame().size)
}

/// Take a panel, a chosen path, and the file dialog options
/// and rewrite the path to utilize the chosen file format.
unsafe fn rewritten_path(
panel: id,
path: id,
options: FileDialogOptions,
) -> (id, Option<FileSpec>) {
let allowed_types = match options.allowed_types {
Some(t) if !t.is_empty() => t,
_ => return (path, None),
};
let accessory: id = msg_send![panel, accessoryView];
if accessory == nil {
return (path, None);
}
let popup_button: id = msg_send![accessory, viewWithTag: FileFormatPopoverTag];
if popup_button == nil {
return (path, None);
}
let index: NSInteger = msg_send![popup_button, indexOfSelectedItem];
let file_spec = allowed_types[index as usize];
let extension = file_spec.extensions[0];

// Remove any extension the user might have entered and replace it with the
// selected extension.
// We're using `NSString` methods instead of `String` simply because
// they are made for this purpose and simplify the implementation.
let path: id = msg_send![path, stringByDeletingPathExtension];
let path: id = msg_send![
path,
stringByAppendingPathExtension: make_nsstring(extension)
];
(path.autorelease(), Some(file_spec))
}
6 changes: 3 additions & 3 deletions druid-shell/src/backend/mac/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use super::menu::Menu;
use super::text_input::NSRange;
use super::util::{assert_main_thread, make_nsstring};
use crate::common_util::IdleCallback;
use crate::dialog::{FileDialogOptions, FileDialogType, FileInfo};
use crate::dialog::{FileDialogOptions, FileDialogType};
use crate::keyboard_types::KeyState;
use crate::mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent};
use crate::region::Region;
Expand Down Expand Up @@ -1165,9 +1165,9 @@ impl WindowHandle {
let token = FileDialogToken::next();
let self_clone = self.clone();
unsafe {
let panel = dialog::build_panel(ty, opts);
let panel = dialog::build_panel(ty, opts.clone());
let block = ConcreteBlock::new(move |response: dialog::NSModalResponse| {
let url = dialog::get_path(panel, response).map(|s| FileInfo { path: s.into() });
let url = dialog::get_file_info(panel, opts.clone(), response);
let view = self_clone.nsview.load();
if let Some(view) = (*view).as_ref() {
let view_state: *mut c_void = *view.get_ivar("viewState");
Expand Down
6 changes: 5 additions & 1 deletion druid-shell/src/backend/windows/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ impl MyWndProc {
.ok()
.map(|os_str| FileInfo {
path: os_str.into(),
format: None,
})
};
self.with_wnd_state(|s| s.handler.save_as(token, info));
Expand All @@ -611,7 +612,10 @@ impl MyWndProc {
let info = unsafe {
get_file_dialog_path(hwnd, FileDialogType::Open, options)
.ok()
.map(|s| FileInfo { path: s.into() })
.map(|s| FileInfo {
path: s.into(),
format: None,
})
};
self.with_wnd_state(|s| s.handler.open_file(token, info));
}
Expand Down
14 changes: 13 additions & 1 deletion druid-shell/src/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,19 @@ use std::path::{Path, PathBuf};
/// This path might point to a file or a directory.
#[derive(Debug, Clone)]
pub struct FileInfo {
pub(crate) path: PathBuf,
/// The path to the selected file.
///
/// On macOS, this is already rewritten to use the extension that the user selected
/// with the `file format` property.
pub path: PathBuf,
/// The selected file format.
///
/// If there're multiple different formats available
/// this allows understanding the kind of format that the user expects the file
/// to be written in. Examples could be Blender 2.4 vs Blender 2.6 vs Blender 2.8.
/// The `path` above will already contain the appropriate extension chosen in the
/// `format` property, so it is not necessary to mutate `path` any further.
pub format: Option<FileSpec>,
}

/// Type of file dialog.
Expand Down