Skip to content

Commit b890126

Browse files
committed
Add more file dialog configuration and macOS support.
1 parent 9aaa883 commit b890126

File tree

4 files changed

+168
-46
lines changed

4 files changed

+168
-46
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i
4848
- `im` feature, with `Data` support for the [`im` crate](https://docs.rs/im/) collections. ([#924] by [@cmyr])
4949
- `im::Vector` support for the `List` widget. ([#940] by [@xStrom])
5050
- `LifeCycle::Size` event to inform widgets that their size changed. ([#953] by [@xStrom])
51+
- `FileDialogOptions` methods `default_name`, `name_label`, `title`, `button_text`, `force_starting_directory`. ([#960] by [@xStrom])
5152

5253
### Changed
5354

@@ -98,6 +99,8 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i
9899
- Routing `LifeCycle::FocusChanged` to descendant widgets. ([#925] by [@yrns])
99100
- Built-in open and save menu items now show the correct label and submit the right commands. ([#930] by [@finnerale])
100101
- Wheel events now properly update hot state. ([#951] by [@xStrom])
102+
- macOS: Support `FileDialogOptions::default_type`. ([#960] by [@xStrom])
103+
- macOS: Show the save dialog even with `FileDialogOptions` `select_directories` and `multi_selection` set. ([#960] by [@xStrom])
101104

102105
### Visual
103106

@@ -198,6 +201,7 @@ This means that druid no longer requires cairo on macOS and uses Core Graphics i
198201
[#951]: https://github.com/xi-editor/druid/pull/951
199202
[#953]: https://github.com/xi-editor/druid/pull/953
200203
[#954]: https://github.com/xi-editor/druid/pull/954
204+
[#960]: https://github.com/xi-editor/druid/pull/960
201205

202206
## [0.5.0] - 2020-04-01
203207

druid-shell/src/dialog.rs

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,17 @@ pub enum FileDialogType {
3333

3434
/// Options for file dialogs.
3535
#[derive(Debug, Clone, Default)]
36-
pub struct FileDialogOptions {
36+
pub struct FileDialogOptions<'a> {
3737
pub show_hidden: bool,
38-
pub allowed_types: Option<Vec<FileSpec>>,
39-
pub default_type: Option<FileSpec>,
38+
pub allowed_types: Option<Vec<FileSpec<'a>>>,
39+
pub default_type: Option<FileSpec<'a>>,
4040
pub select_directories: bool,
4141
pub multi_selection: bool,
42+
pub default_name: Option<&'a str>,
43+
pub name_label: Option<&'a str>,
44+
pub title: Option<&'a str>,
45+
pub button_text: Option<&'a str>,
46+
pub starting_directory: Option<&'a Path>,
4247
// we don't want a library user to be able to construct this type directly
4348
__non_exhaustive: (),
4449
}
@@ -52,7 +57,7 @@ pub struct FileDialogOptions {
5257
///
5358
/// [`COMDLG_FILTERSPEC`]: https://docs.microsoft.com/en-ca/windows/win32/api/shtypes/ns-shtypes-comdlg_filterspec
5459
#[derive(Debug, Clone, Copy, PartialEq)]
55-
pub struct FileSpec {
60+
pub struct FileSpec<'a> {
5661
/// A human readable name, describing this filetype.
5762
///
5863
/// This is used in the Windows file dialog, where the user can select
@@ -61,11 +66,11 @@ pub struct FileSpec {
6166
/// This should not include the file extensions; they will be added automatically.
6267
/// For instance, if we are describing Word documents, the name would be "Word Document",
6368
/// and the displayed string would be "Word Document (*.doc)".
64-
pub name: &'static str,
69+
pub name: &'a str,
6570
/// The file extensions used by this file type.
6671
///
6772
/// This should not include the leading '.'.
68-
pub extensions: &'static [&'static str],
73+
pub extensions: &'a [&'a str],
6974
}
7075

7176
impl FileInfo {
@@ -75,54 +80,108 @@ impl FileInfo {
7580
}
7681
}
7782

78-
impl FileDialogOptions {
83+
impl<'a> FileDialogOptions<'a> {
7984
/// Create a new set of options.
80-
pub fn new() -> FileDialogOptions {
85+
pub fn new() -> FileDialogOptions<'static> {
8186
FileDialogOptions::default()
8287
}
8388

84-
/// Set the 'show hidden files' bit.
89+
/// Set whether hidden files and folders are shown.
90+
///
91+
/// # macOS
92+
///
93+
/// This option only shows hidden files, folders remain hidden.
8594
pub fn show_hidden(mut self) -> Self {
8695
self.show_hidden = true;
8796
self
8897
}
8998

90-
/// Set whether folders should be selectable.
99+
/// Set whether folders should be selectable instead of files.
100+
///
101+
/// This is only relevant for open dialogs.
91102
pub fn select_directories(mut self) -> Self {
92103
self.select_directories = true;
93104
self
94105
}
95106

96-
/// Set whether multiple files can be selected.
107+
/// Set whether multiple items can be selected.
108+
///
109+
/// This is only relevant for open dialogs.
97110
pub fn multi_selection(mut self) -> Self {
98111
self.multi_selection = true;
99112
self
100113
}
101114

102115
/// Set the file types the user is allowed to select.
103-
pub fn allowed_types(mut self, types: Vec<FileSpec>) -> Self {
104-
self.allowed_types = Some(types);
116+
///
117+
/// An empty collection is treated as no filter.
118+
pub fn allowed_types(mut self, types: Vec<FileSpec<'a>>) -> Self {
119+
// An empty vector can cause platform issues, so treat it as no filter
120+
if types.is_empty() {
121+
self.allowed_types = None;
122+
} else {
123+
self.allowed_types = Some(types);
124+
}
105125
self
106126
}
107127

108128
/// Set the default file type.
109-
/// If it's `None` or not present in [`allowed_types`](#method.allowed_types)
110-
/// then the first entry in [`allowed_types`](#method.allowed_types) will be used as default.
111-
pub fn default_type(mut self, default_type: FileSpec) -> Self {
129+
///
130+
/// The provided `FileSpec` must be also present in [`allowed_types`].
131+
///
132+
/// If it's `None` then the first entry in [`allowed_types`] will be used as the default.
133+
///
134+
/// [`allowed_types`]: #method.allowed_types
135+
pub fn default_type(mut self, default_type: FileSpec<'a>) -> Self {
112136
self.default_type = Some(default_type);
113137
self
114138
}
139+
140+
/// Set the default filename that appears in the dialog.
141+
pub fn default_name(mut self, default_name: &'a str) -> Self {
142+
self.default_name = Some(default_name);
143+
self
144+
}
145+
146+
/// Set the text in the label next to the filename editbox.
147+
pub fn name_label(mut self, name_label: &'a str) -> Self {
148+
self.name_label = Some(name_label);
149+
self
150+
}
151+
152+
/// Set the title text of the dialog.
153+
pub fn title(mut self, title: &'a str) -> Self {
154+
self.title = Some(title);
155+
self
156+
}
157+
158+
/// Set the text of the Open/Save button.
159+
pub fn button_text(mut self, text: &'a str) -> Self {
160+
self.button_text = Some(text);
161+
self
162+
}
163+
164+
/// Force the starting folder to the specified `Path`.
165+
///
166+
/// # User experience
167+
///
168+
/// This should almost never be used because it overrides the OS choice,
169+
/// which will usually be a folder that the user recently visited.
170+
pub fn force_starting_directory(mut self, path: &'a Path) -> Self {
171+
self.starting_directory = Some(path);
172+
self
173+
}
115174
}
116175

117-
impl FileSpec {
118-
pub const TEXT: FileSpec = FileSpec::new("Text", &["txt"]);
119-
pub const JPG: FileSpec = FileSpec::new("Jpeg", &["jpg", "jpeg"]);
120-
pub const GIF: FileSpec = FileSpec::new("Gif", &["gif"]);
121-
pub const PDF: FileSpec = FileSpec::new("PDF", &["pdf"]);
122-
pub const HTML: FileSpec = FileSpec::new("Web Page", &["htm", "html"]);
176+
impl<'a> FileSpec<'a> {
177+
pub const TEXT: FileSpec<'a> = FileSpec::new("Text", &["txt"]);
178+
pub const JPG: FileSpec<'a> = FileSpec::new("Jpeg", &["jpg", "jpeg"]);
179+
pub const GIF: FileSpec<'a> = FileSpec::new("Gif", &["gif"]);
180+
pub const PDF: FileSpec<'a> = FileSpec::new("PDF", &["pdf"]);
181+
pub const HTML: FileSpec<'a> = FileSpec::new("Web Page", &["htm", "html"]);
123182

124183
/// Create a new `FileSpec`.
125-
pub const fn new(name: &'static str, extensions: &'static [&'static str]) -> Self {
184+
pub const fn new(name: &'a str, extensions: &'a [&'a str]) -> Self {
126185
FileSpec { name, extensions }
127186
}
128187
}

druid-shell/src/platform/mac/dialog.rs

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
use std::ffi::OsString;
2020

21-
use cocoa::base::{id, nil, YES};
22-
use cocoa::foundation::{NSArray, NSInteger};
21+
use cocoa::base::{id, nil, NO, YES};
22+
use cocoa::foundation::{NSArray, NSAutoreleasePool, NSInteger, NSURL};
2323
use objc::{class, msg_send, sel, sel_impl};
2424

2525
use super::util::{from_nsstring, make_nsstring};
@@ -28,42 +28,92 @@ use crate::dialog::{FileDialogOptions, FileDialogType};
2828
const NSModalResponseOK: NSInteger = 1;
2929
const NSModalResponseCancel: NSInteger = 0;
3030

31+
#[allow(clippy::cognitive_complexity)]
3132
pub(crate) fn get_file_dialog_path(
3233
ty: FileDialogType,
33-
options: FileDialogOptions,
34+
mut options: FileDialogOptions,
3435
) -> Option<OsString> {
3536
unsafe {
3637
let panel: id = match ty {
3738
FileDialogType::Open => msg_send![class!(NSOpenPanel), openPanel],
3839
FileDialogType::Save => msg_send![class!(NSSavePanel), savePanel],
3940
};
4041

41-
// set options
42+
// Set open dialog specific options
43+
// NSOpenPanel inherits from NSSavePanel and thus has more options.
44+
let mut set_type_filter = true;
45+
if let FileDialogType::Open = ty {
46+
if options.select_directories {
47+
let () = msg_send![panel, setCanChooseDirectories: YES];
48+
// Disable the selection of files in directory selection mode,
49+
// because other platforms like Windows have no support for it,
50+
// and expecting it to work will lead to buggy cross-platform behavior.
51+
let () = msg_send![panel, setCanChooseFiles: NO];
52+
// Because we're choosing only directories, file filters don't make sense.
53+
set_type_filter = false;
54+
}
55+
if options.multi_selection {
56+
let () = msg_send![panel, setAllowsMultipleSelection: YES];
57+
}
58+
}
59+
60+
// Set universal options
4261
if options.show_hidden {
4362
let () = msg_send![panel, setShowsHiddenFiles: YES];
4463
}
4564

46-
if options.select_directories {
47-
let () = msg_send![panel, setCanChooseDirectories: YES];
65+
if let Some(default_name) = options.default_name {
66+
let () = msg_send![panel, setNameFieldStringValue: make_nsstring(default_name)];
67+
}
68+
69+
if let Some(name_label) = options.name_label {
70+
let () = msg_send![panel, setNameFieldLabel: make_nsstring(name_label)];
71+
}
72+
73+
if let Some(title) = options.title {
74+
let () = msg_send![panel, setTitle: make_nsstring(title)];
4875
}
4976

50-
if options.multi_selection {
51-
let () = msg_send![panel, setAllowsMultipleSelection: YES];
77+
if let Some(text) = options.button_text {
78+
let () = msg_send![panel, setPrompt: make_nsstring(text)];
5279
}
5380

54-
// A vector of NSStrings. this must outlive `nsarray_allowed_types`.
55-
let allowed_types = options.allowed_types.as_ref().map(|specs| {
56-
specs
57-
.iter()
58-
.flat_map(|spec| spec.extensions.iter().map(|s| make_nsstring(s)))
59-
.collect::<Vec<_>>()
60-
});
61-
62-
let nsarray_allowed_types = allowed_types
63-
.as_ref()
64-
.map(|types| NSArray::arrayWithObjects(nil, types.as_slice()));
65-
if let Some(nsarray) = nsarray_allowed_types {
66-
let () = msg_send![panel, setAllowedFileTypes: nsarray];
81+
if let Some(path) = options.starting_directory {
82+
if let Some(path) = path.to_str() {
83+
let url = NSURL::alloc(nil)
84+
.initFileURLWithPath_isDirectory_(make_nsstring(path), YES)
85+
.autorelease();
86+
let () = msg_send![panel, setDirectoryURL: url];
87+
}
88+
}
89+
90+
if set_type_filter {
91+
// If a default type was specified, then we must reorder the allowed types,
92+
// because there's no way to specify the default type other than having it be first.
93+
if let Some(allowed_types) = options.allowed_types.as_mut() {
94+
if let Some(dt) = options.default_type {
95+
if let Some(idx) = allowed_types.iter().position(|t| *t == dt) {
96+
allowed_types.swap(idx, 0);
97+
} else {
98+
log::warn!("The default type {:?} is not present in allowed types.", dt);
99+
}
100+
}
101+
}
102+
103+
// A vector of NSStrings. this must outlive `nsarray_allowed_types`.
104+
let allowed_types = options.allowed_types.as_ref().map(|specs| {
105+
specs
106+
.iter()
107+
.flat_map(|spec| spec.extensions.iter().map(|s| make_nsstring(s)))
108+
.collect::<Vec<_>>()
109+
});
110+
111+
let nsarray_allowed_types = allowed_types
112+
.as_ref()
113+
.map(|types| NSArray::arrayWithObjects(nil, types.as_slice()));
114+
if let Some(nsarray) = nsarray_allowed_types {
115+
let () = msg_send![panel, setAllowedFileTypes: nsarray];
116+
}
67117
}
68118

69119
let result: NSInteger = msg_send![panel, runModal];

druid/examples/open_save.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,17 @@ fn ui_builder() -> impl Widget<String> {
3737
let other = FileSpec::new("Bogus file", &["foo", "bar", "baz"]);
3838
let save_dialog_options = FileDialogOptions::new()
3939
.allowed_types(vec![rs, txt, other])
40-
.default_type(txt);
41-
let open_dialog_options = save_dialog_options.clone();
40+
.default_type(txt)
41+
.default_name("MyFile.txt")
42+
.name_label("Target")
43+
.title("Choose a target for this lovely file")
44+
.button_text("Export");
45+
let open_dialog_options = save_dialog_options
46+
.clone()
47+
.default_name("MySavedFile.txt")
48+
.name_label("Source")
49+
.title("Where did you put that file?")
50+
.button_text("Import");
4251

4352
let input = TextBox::new();
4453
let save = Button::new("Save").on_click(move |ctx, _, _| {

0 commit comments

Comments
 (0)