cosmic/dialog/file_chooser/
open.rs

1// Copyright 2023 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! Request to open files and/or directories.
5//!
6//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog)
7//! example in our repository.
8
9#[cfg(feature = "xdg-portal")]
10pub use portal::{FileResponse, MultiFileResponse, file, files, folder, folders};
11
12#[cfg(feature = "rfd")]
13pub use rust_fd::{FileResponse, MultiFileResponse, file, files, folder, folders};
14
15use super::Error;
16use std::path::PathBuf;
17
18/// A builder for an open file dialog
19#[derive(derive_setters::Setters)]
20#[must_use]
21pub struct Dialog {
22    /// The label for the dialog's window title.
23    #[setters(into)]
24    title: String,
25
26    /// The label for the accept button. Mnemonic underlines are allowed.
27    #[cfg(feature = "xdg-portal")]
28    #[setters(skip)]
29    accept_label: Option<String>,
30
31    /// Sets the starting directory of the dialog.
32    #[setters(into, strip_option)]
33    #[allow(dead_code)] // TODO: ashpd does not expose this yet
34    directory: Option<PathBuf>,
35
36    /// Set starting file name of the dialog.
37    #[setters(into, strip_option)]
38    #[allow(dead_code)] // TODO: ashpd does not expose this yet
39    file_name: Option<String>,
40
41    /// Modal dialogs require user input before continuing the program.
42    #[cfg(feature = "xdg-portal")]
43    #[setters(skip)]
44    modal: bool,
45
46    /// Adds a list of choices.
47    #[cfg(feature = "xdg-portal")]
48    #[setters(skip)]
49    choices: Vec<super::Choice>,
50
51    /// Specifies the default file filter.
52    #[cfg(feature = "xdg-portal")]
53    #[setters(skip)]
54    current_filter: Option<super::FileFilter>,
55
56    /// A collection of file filters.
57    #[setters(skip)]
58    pub(self) filters: Vec<super::FileFilter>,
59}
60
61impl Dialog {
62    pub const fn new() -> Self {
63        Self {
64            title: String::new(),
65            #[cfg(feature = "xdg-portal")]
66            accept_label: None,
67            directory: None,
68            file_name: None,
69            #[cfg(feature = "xdg-portal")]
70            modal: true,
71            #[cfg(feature = "xdg-portal")]
72            current_filter: None,
73            #[cfg(feature = "xdg-portal")]
74            choices: Vec::new(),
75            filters: Vec::new(),
76        }
77    }
78
79    /// The label for the accept button. Mnemonic underlines are allowed.
80    #[cfg(feature = "xdg-portal")]
81    pub fn accept_label(mut self, label: impl Into<String>) -> Self {
82        self.accept_label = Some(label.into());
83        self
84    }
85
86    /// Adds a choice.
87    #[cfg(feature = "xdg-portal")]
88    pub fn choice(mut self, choice: impl Into<super::Choice>) -> Self {
89        self.choices.push(choice.into());
90        self
91    }
92
93    /// Specifies the default file filter.
94    #[cfg(feature = "xdg-portal")]
95    pub fn current_filter(mut self, filter: impl Into<super::FileFilter>) -> Self {
96        self.current_filter = Some(filter.into());
97        self
98    }
99
100    /// Adds a files filter.
101    pub fn filter(mut self, filter: impl Into<super::FileFilter>) -> Self {
102        self.filters.push(filter.into());
103        self
104    }
105
106    /// Modal dialogs require user input before continuing the program.
107    #[cfg(feature = "xdg-portal")]
108    pub fn modal(mut self, modal: bool) -> Self {
109        self.modal = modal;
110        self
111    }
112
113    /// Create an open file dialog.
114    pub async fn open_file(self) -> Result<FileResponse, Error> {
115        file(self).await
116    }
117
118    /// Create an open file dialog with multiple file select.
119    pub async fn open_files(self) -> Result<MultiFileResponse, Error> {
120        files(self).await
121    }
122
123    /// Create an open folder dialog.
124    pub async fn open_folder(self) -> Result<FileResponse, Error> {
125        folder(self).await
126    }
127
128    /// Create an open folder dialog with multi file select.
129    pub async fn open_folders(self) -> Result<MultiFileResponse, Error> {
130        folders(self).await
131    }
132}
133
134#[cfg(feature = "xdg-portal")]
135mod portal {
136    use super::Dialog;
137    use crate::dialog::file_chooser::Error;
138    use ashpd::desktop::file_chooser::SelectedFiles;
139    use url::Url;
140
141    fn error_or_cancel(error: ashpd::Error) -> Error {
142        if let ashpd::Error::Response(ashpd::desktop::ResponseError::Cancelled) = error {
143            Error::Cancelled
144        } else {
145            Error::Open(error)
146        }
147    }
148
149    /// Creates a new file dialog, and begins to await its responses.
150    #[cfg(feature = "xdg-portal")]
151    pub async fn create(
152        dialog: super::Dialog,
153        folders: bool,
154        multiple: bool,
155    ) -> Result<ashpd::desktop::Request<SelectedFiles>, Error> {
156        // TODO: Set window identifier
157        ashpd::desktop::file_chooser::OpenFileRequest::default()
158            .title(Some(dialog.title.as_str()))
159            .accept_label(dialog.accept_label.as_deref())
160            .directory(folders)
161            .modal(dialog.modal)
162            .multiple(multiple)
163            .choices(dialog.choices)
164            .filters(dialog.filters)
165            .current_filter(dialog.current_filter)
166            .send()
167            .await
168            .map_err(error_or_cancel)
169    }
170
171    fn file_response(
172        request: ashpd::desktop::Request<SelectedFiles>,
173    ) -> Result<FileResponse, Error> {
174        request
175            .response()
176            .map(FileResponse)
177            .map_err(error_or_cancel)
178    }
179
180    fn multi_file_response(
181        request: ashpd::desktop::Request<SelectedFiles>,
182    ) -> Result<MultiFileResponse, Error> {
183        request
184            .response()
185            .map(MultiFileResponse)
186            .map_err(error_or_cancel)
187    }
188
189    pub async fn file(dialog: Dialog) -> Result<FileResponse, Error> {
190        file_response(create(dialog, false, false).await?)
191    }
192
193    pub async fn files(dialog: Dialog) -> Result<MultiFileResponse, Error> {
194        multi_file_response(create(dialog, false, true).await?)
195    }
196
197    pub async fn folder(dialog: Dialog) -> Result<FileResponse, Error> {
198        file_response(create(dialog, true, false).await?)
199    }
200
201    pub async fn folders(dialog: Dialog) -> Result<MultiFileResponse, Error> {
202        multi_file_response(create(dialog, true, true).await?)
203    }
204
205    /// A dialog response containing the selected file or folder.
206    pub struct FileResponse(pub SelectedFiles);
207
208    impl FileResponse {
209        pub fn choices(&self) -> &[(String, String)] {
210            self.0.choices()
211        }
212
213        pub fn url(&self) -> &Url {
214            self.0.uris().first().expect("no files selected")
215        }
216    }
217
218    /// A dialog response containing the selected file(s) or folder(s).
219    pub struct MultiFileResponse(pub SelectedFiles);
220
221    impl MultiFileResponse {
222        pub fn choices(&self) -> &[(String, String)] {
223            self.0.choices()
224        }
225
226        pub fn urls(&self) -> &[Url] {
227            self.0.uris()
228        }
229    }
230}
231
232#[cfg(feature = "rfd")]
233mod rust_fd {
234    use super::Dialog;
235    use crate::dialog::file_chooser::Error;
236    use url::Url;
237
238    pub fn create(dialog: Dialog) -> rfd::AsyncFileDialog {
239        let mut builder = rfd::AsyncFileDialog::new().set_title(dialog.title);
240
241        if let Some(directory) = dialog.directory {
242            builder = builder.set_directory(directory);
243        }
244
245        if let Some(file_name) = dialog.file_name {
246            builder = builder.set_file_name(file_name);
247        }
248
249        for filter in dialog.filters {
250            builder = builder.add_filter(filter.description, &filter.extensions);
251        }
252
253        builder
254    }
255
256    fn file_response(request: Option<rfd::FileHandle>) -> Result<FileResponse, Error> {
257        if let Some(handle) = request {
258            let url = Url::from_file_path(handle.path()).map_err(|_| Error::UrlAbsolute)?;
259
260            return Ok(FileResponse(url));
261        }
262
263        Err(Error::Cancelled)
264    }
265
266    fn multi_file_response(
267        request: Option<Vec<rfd::FileHandle>>,
268    ) -> Result<MultiFileResponse, Error> {
269        if let Some(handles) = request {
270            let mut urls = Vec::with_capacity(handles.len());
271
272            for handle in &handles {
273                urls.push(Url::from_file_path(handle.path()).map_err(|()| Error::UrlAbsolute)?);
274            }
275
276            return Ok(MultiFileResponse(urls));
277        }
278
279        Err(Error::Cancelled)
280    }
281
282    pub async fn file(dialog: Dialog) -> Result<FileResponse, Error> {
283        file_response(create(dialog).pick_file().await)
284    }
285
286    pub async fn files(dialog: Dialog) -> Result<MultiFileResponse, Error> {
287        multi_file_response(create(dialog).pick_files().await)
288    }
289
290    pub async fn folder(dialog: Dialog) -> Result<FileResponse, Error> {
291        file_response(create(dialog).pick_folder().await)
292    }
293
294    pub async fn folders(dialog: Dialog) -> Result<MultiFileResponse, Error> {
295        multi_file_response(create(dialog).pick_folders().await)
296    }
297
298    /// A dialog response containing the selected file or folder.
299    pub struct FileResponse(Url);
300
301    impl FileResponse {
302        pub fn choices(&self) -> &[(String, String)] {
303            &[]
304        }
305
306        pub fn url(&self) -> &Url {
307            &self.0
308        }
309    }
310
311    /// A dialog response containing the selected file(s) or folder(s).
312    pub struct MultiFileResponse(Vec<Url>);
313
314    impl MultiFileResponse {
315        pub fn choices(&self) -> &[(String, String)] {
316            &[]
317        }
318
319        pub fn urls(&self) -> &[Url] {
320            &self.0
321        }
322    }
323}