druid/dialog.rs
1// Copyright 2020 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Configuration for open and save file dialogs.
16//!
17//! This is a wrapper around [`druid_shell::FileDialogOptions`] with a few extra Druid specifics.
18//! As such, many of the docs are copied from `druid_shell`, and should be kept in sync.
19
20use std::path::PathBuf;
21
22use druid_shell::FileDialogOptions as ShellOptions;
23
24use crate::{FileInfo, FileSpec, Selector};
25
26/// Options for file dialogs.
27///
28/// File dialogs let the user choose a specific path to open or save.
29///
30/// By default the file dialogs operate in *files mode* where the user can only choose files.
31/// Importantly these are files from the user's perspective, but technically the returned path
32/// will be a directory when the user chooses a package. You can read more about [packages] below.
33/// It's also possible for users to manually specify a path which they might otherwise not be able
34/// to choose. Thus it is important to verify that all the returned paths match your expectations.
35///
36/// The open dialog can also be switched to *directories mode* via [`select_directories`].
37///
38/// # Cross-platform compatibility
39///
40/// You could write platform specific code that really makes the best use of each platform.
41/// However if you want to write universal code that will work on all platforms then
42/// you have to keep some restrictions in mind.
43///
44/// ## Don't depend on directories with extensions
45///
46/// Your application should avoid having to deal with directories that have extensions
47/// in their name, e.g. `my_stuff.pkg`. This clashes with [packages] on macOS and you
48/// will either need platform specific code or a degraded user experience on macOS
49/// via [`packages_as_directories`].
50///
51/// ## Use the save dialog only for new paths
52///
53/// Don't direct the user to choose an existing file with the save dialog.
54/// Selecting existing files for overwriting is possible but extremely cumbersome on macOS.
55/// The much more optimized flow is to have the user select a file with the open dialog
56/// and then keep saving to that file without showing a save dialog.
57/// Use the save dialog only for selecting a new location.
58///
59/// # macOS
60///
61/// The file dialog works a bit differently on macOS. For a lot of applications this doesn't matter
62/// and you don't need to know the details. However if your application makes extensive use
63/// of file dialogs and you target macOS then you should understand the macOS specifics.
64///
65/// ## Packages
66///
67/// On macOS directories with known extensions are considered to be packages, e.g. `app_files.pkg`.
68/// Furthermore the packages are divided into two groups based on their extension.
69/// First there are packages that have been defined at the OS level, and secondly there are
70/// packages that are defined at the file dialog level based on [`allowed_types`].
71/// These two types have slightly different behavior in the file dialogs. Generally packages
72/// behave similarly to regular files in many contexts, including the file dialogs.
73/// This package concept can be turned off in the file dialog via [`packages_as_directories`].
74///
75///  | Packages as files. File filters apply to packages. | Packages as directories.
76/// -------- | -------------------------------------------------- | ------------------------
77/// Open directory | Not selectable. Not traversable. | Selectable. Traversable.
78/// Open file | Selectable. Not traversable. | Not selectable. Traversable.
79/// Save file | OS packages [clickable] but not traversable.<br/>Dialog packages traversable but not selectable. | Not selectable. Traversable.
80///
81/// Keep in mind that the file dialog may start inside any package if the user has traversed
82/// into one just recently. The user might also manually specify a path inside a package.
83///
84/// Generally this behavior should be kept, because it's least surprising to macOS users.
85/// However if your application requires selecting directories with extensions as directories
86/// or the user needs to be able to traverse into them to select a specific file,
87/// then you can change the default behavior via [`packages_as_directories`]
88/// to force macOS to behave like other platforms and not give special treatment to packages.
89///
90/// ## Selecting files for overwriting in the save dialog is cumbersome
91///
92/// Existing files can be clicked on in the save dialog, but that only copies their base file name.
93/// If the clicked file's extension is different than the first extension of the default type
94/// then the returned path does not actually match the path of the file that was clicked on.
95/// Clicking on a file doesn't change the base path either. Keep in mind that the macOS file dialog
96/// can have several directories open at once. So if a user has traversed into `/Users/Joe/foo/`
97/// and then clicks on an existing file `/Users/Joe/old.txt` in another directory then the returned
98/// path will actually be `/Users/Joe/foo/old.rtf` if the default type's first extension is `rtf`.
99///
100/// ## Have a really good save dialog default type
101///
102/// There is no way for the user to choose which extension they want to save a file as via the UI.
103/// They have no way of knowing which extensions are even supported and must manually type it out.
104///
105/// *Hopefully it's a temporary problem and we can find a way to show the file formats in the UI.
106/// This is being tracked in [druid#998].*
107///
108/// [clickable]: #selecting-files-for-overwriting-in-the-save-dialog-is-cumbersome
109/// [packages]: #packages
110/// [`select_directories`]: #method.select_directories
111/// [`allowed_types`]: #method.allowed_types
112/// [`packages_as_directories`]: #method.packages_as_directories
113/// [druid#998]: https://github.com/xi-editor/druid/issues/998
114#[derive(Debug, Clone, Default)]
115pub struct FileDialogOptions {
116 pub(crate) opt: ShellOptions,
117 pub(crate) accept_cmd: Option<Selector<FileInfo>>,
118 pub(crate) accept_multiple_cmd: Option<Selector<Vec<FileInfo>>>,
119 pub(crate) cancel_cmd: Option<Selector<()>>,
120}
121
122impl FileDialogOptions {
123 /// Create a new set of options.
124 pub fn new() -> FileDialogOptions {
125 FileDialogOptions::default()
126 }
127
128 /// Set hidden files and directories to be visible.
129 pub fn show_hidden(mut self) -> Self {
130 self.opt = self.opt.show_hidden();
131 self
132 }
133
134 /// Set directories to be selectable instead of files.
135 ///
136 /// This is only relevant for open dialogs.
137 pub fn select_directories(mut self) -> Self {
138 self.opt = self.opt.select_directories();
139 self
140 }
141
142 /// Set [packages] to be treated as directories instead of files.
143 ///
144 /// This allows for writing more universal cross-platform code at the cost of user experience.
145 ///
146 /// This is only relevant on macOS.
147 ///
148 /// [packages]: #packages
149 pub fn packages_as_directories(mut self) -> Self {
150 self.opt = self.opt.packages_as_directories();
151 self
152 }
153
154 /// Set multiple items to be selectable.
155 ///
156 /// This is only relevant for open dialogs.
157 pub fn multi_selection(mut self) -> Self {
158 self.opt = self.opt.multi_selection();
159 self
160 }
161
162 /// Set the file types the user is allowed to select.
163 ///
164 /// This filter is only applied to files and [packages], but not to directories.
165 ///
166 /// An empty collection is treated as no filter.
167 ///
168 /// # macOS
169 ///
170 /// These file types also apply to directories to define [packages].
171 /// Which means the directories that match the filter are no longer considered directories.
172 /// The packages are defined by this collection even in *directories mode*.
173 ///
174 /// [packages]: #packages
175 pub fn allowed_types(mut self, types: Vec<FileSpec>) -> Self {
176 self.opt = self.opt.allowed_types(types);
177 self
178 }
179
180 /// Set the default file type.
181 ///
182 /// The provided `default_type` must also be present in [`allowed_types`].
183 ///
184 /// If it's `None` then the first entry in [`allowed_types`] will be used as the default.
185 ///
186 /// This is only relevant in *files mode*.
187 ///
188 /// [`allowed_types`]: #method.allowed_types
189 pub fn default_type(mut self, default_type: FileSpec) -> Self {
190 self.opt = self.opt.default_type(default_type);
191 self
192 }
193
194 /// Set the default filename that appears in the dialog.
195 pub fn default_name(mut self, default_name: impl Into<String>) -> Self {
196 self.opt = self.opt.default_name(default_name);
197 self
198 }
199
200 /// Set the text in the label next to the filename editbox.
201 pub fn name_label(mut self, name_label: impl Into<String>) -> Self {
202 self.opt = self.opt.name_label(name_label);
203 self
204 }
205
206 /// Set the title text of the dialog.
207 pub fn title(mut self, title: impl Into<String>) -> Self {
208 self.opt = self.opt.title(title);
209 self
210 }
211
212 /// Set the text of the Open/Save button.
213 pub fn button_text(mut self, text: impl Into<String>) -> Self {
214 self.opt = self.opt.button_text(text);
215 self
216 }
217
218 /// Force the starting directory to the specified `path`.
219 ///
220 /// # User experience
221 ///
222 /// This should almost never be used because it overrides the OS choice,
223 /// which will usually be a directory that the user recently visited.
224 pub fn force_starting_directory(mut self, path: impl Into<PathBuf>) -> Self {
225 self.opt = self.opt.force_starting_directory(path);
226 self
227 }
228
229 /// Sets a custom command to use when the file dialog succeeds.
230 ///
231 /// By default, an "open" dialog sends the [`OPEN_FILE`] command when it succeeds, and a "save"
232 /// dialog sends the [`SAVE_FILE_AS`] command. Using this method, you can configure a different
233 /// command to be used.
234 ///
235 /// [`OPEN_FILE`]: crate::commands::OPEN_FILE
236 /// [`SAVE_FILE_AS`]: crate::commands::SAVE_FILE_AS
237 pub fn accept_command(mut self, cmd: Selector<FileInfo>) -> Self {
238 self.accept_cmd = Some(cmd);
239 self
240 }
241
242 /// Sets a custom command to use when the file dialog succeeds with multi selection.
243 ///
244 /// This only works for "open" dialogs configured for multiselection.
245 pub fn accept_multiple_command(mut self, cmd: Selector<Vec<FileInfo>>) -> Self {
246 self.accept_multiple_cmd = Some(cmd);
247 self
248 }
249
250 /// Sets a custom command to use when the file dialog is cancelled.
251 ///
252 /// By default, an "open" dialog sends the [`OPEN_PANEL_CANCELLED`] command when it is cancelled, and a "save"
253 /// dialog sends the [`SAVE_PANEL_CANCELLED`] command. Using this method, you can configure a different
254 /// command to be used.
255 ///
256 /// [`OPEN_PANEL_CANCELLED`]: crate::commands::OPEN_PANEL_CANCELLED
257 /// [`SAVE_PANEL_CANCELLED`]: crate::commands::SAVE_PANEL_CANCELLED
258 pub fn cancel_command(mut self, cmd: Selector<()>) -> Self {
259 self.cancel_cmd = Some(cmd);
260 self
261 }
262}