Skip to main content

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(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(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(xdg_portal)]
43    #[setters(skip)]
44    modal: bool,
45
46    /// Adds a list of choices.
47    #[cfg(xdg_portal)]
48    #[setters(skip)]
49    choices: Vec<super::Choice>,
50
51    /// Specifies the default file filter.
52    #[cfg(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(xdg_portal)]
66            accept_label: None,
67            directory: None,
68            file_name: None,
69            #[cfg(xdg_portal)]
70            modal: true,
71            #[cfg(xdg_portal)]
72            current_filter: None,
73            #[cfg(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(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(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(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(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(xdg_portal)]
135mod portal {
136    use super::Dialog;
137    use crate::dialog::file_chooser::{Error, parse_uri};
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(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        let files = request.response().map_err(error_or_cancel)?;
175        let url = parse_uri(files.uris().first().ok_or(Error::Cancelled)?)?;
176        Ok(FileResponse { files, url })
177    }
178
179    fn multi_file_response(
180        request: ashpd::desktop::Request<SelectedFiles>,
181    ) -> Result<MultiFileResponse, Error> {
182        let files = request.response().map_err(error_or_cancel)?;
183        let urls = files
184            .uris()
185            .iter()
186            .map(parse_uri)
187            .collect::<Result<Vec<Url>, _>>()?;
188        Ok(MultiFileResponse { files, urls })
189    }
190
191    pub async fn file(dialog: Dialog) -> Result<FileResponse, Error> {
192        file_response(create(dialog, false, false).await?)
193    }
194
195    pub async fn files(dialog: Dialog) -> Result<MultiFileResponse, Error> {
196        multi_file_response(create(dialog, false, true).await?)
197    }
198
199    pub async fn folder(dialog: Dialog) -> Result<FileResponse, Error> {
200        file_response(create(dialog, true, false).await?)
201    }
202
203    pub async fn folders(dialog: Dialog) -> Result<MultiFileResponse, Error> {
204        multi_file_response(create(dialog, true, true).await?)
205    }
206
207    /// A dialog response containing the selected file or folder.
208    pub struct FileResponse {
209        pub files: SelectedFiles,
210        url: Url,
211    }
212
213    impl FileResponse {
214        pub fn choices(&self) -> &[(String, String)] {
215            self.files.choices()
216        }
217
218        pub fn url(&self) -> &Url {
219            &self.url
220        }
221    }
222
223    /// A dialog response containing the selected file(s) or folder(s).
224    pub struct MultiFileResponse {
225        pub files: SelectedFiles,
226        urls: Vec<Url>,
227    }
228
229    impl MultiFileResponse {
230        pub fn choices(&self) -> &[(String, String)] {
231            self.files.choices()
232        }
233
234        pub fn urls(&self) -> &[Url] {
235            &self.urls
236        }
237    }
238}
239
240#[cfg(feature = "rfd")]
241mod rust_fd {
242    use super::Dialog;
243    use crate::dialog::file_chooser::Error;
244    use url::Url;
245
246    pub fn create(dialog: Dialog) -> rfd::AsyncFileDialog {
247        let mut builder = rfd::AsyncFileDialog::new().set_title(dialog.title);
248
249        if let Some(directory) = dialog.directory {
250            builder = builder.set_directory(directory);
251        }
252
253        if let Some(file_name) = dialog.file_name {
254            builder = builder.set_file_name(file_name);
255        }
256
257        for filter in dialog.filters {
258            builder = builder.add_filter(filter.description, &filter.extensions);
259        }
260
261        builder
262    }
263
264    fn file_response(request: Option<rfd::FileHandle>) -> Result<FileResponse, Error> {
265        if let Some(handle) = request {
266            let url = Url::from_file_path(handle.path()).map_err(|_| Error::UrlAbsolute)?;
267
268            return Ok(FileResponse(url));
269        }
270
271        Err(Error::Cancelled)
272    }
273
274    fn multi_file_response(
275        request: Option<Vec<rfd::FileHandle>>,
276    ) -> Result<MultiFileResponse, Error> {
277        if let Some(handles) = request {
278            let mut urls = Vec::with_capacity(handles.len());
279
280            for handle in &handles {
281                urls.push(Url::from_file_path(handle.path()).map_err(|()| Error::UrlAbsolute)?);
282            }
283
284            return Ok(MultiFileResponse(urls));
285        }
286
287        Err(Error::Cancelled)
288    }
289
290    pub async fn file(dialog: Dialog) -> Result<FileResponse, Error> {
291        file_response(create(dialog).pick_file().await)
292    }
293
294    pub async fn files(dialog: Dialog) -> Result<MultiFileResponse, Error> {
295        multi_file_response(create(dialog).pick_files().await)
296    }
297
298    pub async fn folder(dialog: Dialog) -> Result<FileResponse, Error> {
299        file_response(create(dialog).pick_folder().await)
300    }
301
302    pub async fn folders(dialog: Dialog) -> Result<MultiFileResponse, Error> {
303        multi_file_response(create(dialog).pick_folders().await)
304    }
305
306    /// A dialog response containing the selected file or folder.
307    pub struct FileResponse(Url);
308
309    impl FileResponse {
310        pub fn choices(&self) -> &[(String, String)] {
311            &[]
312        }
313
314        pub fn url(&self) -> &Url {
315            &self.0
316        }
317    }
318
319    /// A dialog response containing the selected file(s) or folder(s).
320    pub struct MultiFileResponse(Vec<Url>);
321
322    impl MultiFileResponse {
323        pub fn choices(&self) -> &[(String, String)] {
324            &[]
325        }
326
327        pub fn urls(&self) -> &[Url] {
328            &self.0
329        }
330    }
331}