Skip to main content

winio_ui_app_kit/dialogs/
filebox.rs

1use std::{cell::Cell, path::PathBuf, rc::Rc};
2
3use block2::StackBlock;
4use futures_util::{FutureExt, TryFutureExt, future::Either};
5use objc2::{MainThreadOnly, rc::Retained};
6use objc2_app_kit::{NSModalResponseOK, NSOpenPanel, NSSavePanel};
7use objc2_foundation::{MainThreadMarker, NSArray, NSString};
8use objc2_uniform_type_identifiers::UTType;
9use winio_handle::AsWindow;
10
11use crate::{Error, Result, catch, from_nsstring};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct FileFilter {
15    name: String,
16    pattern: String,
17}
18
19impl FileFilter {
20    pub fn new(name: &str, pattern: &str) -> Self {
21        Self {
22            name: name.to_string(),
23            pattern: pattern.to_string(),
24        }
25    }
26}
27
28#[derive(Debug, Default, Clone)]
29pub struct FileBox {
30    title: Retained<NSString>,
31    filename: Retained<NSString>,
32    filters: Vec<FileFilter>,
33}
34
35// SAFETY: NSString is thread-safe.
36unsafe impl Send for FileBox {}
37unsafe impl Sync for FileBox {}
38
39impl FileBox {
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    pub fn title(&mut self, title: &str) {
45        self.title = NSString::from_str(title);
46    }
47
48    pub fn filename(&mut self, filename: &str) {
49        self.filename = NSString::from_str(filename);
50    }
51
52    pub fn filters(&mut self, filters: impl IntoIterator<Item = FileFilter>) {
53        self.filters = filters.into_iter().collect();
54    }
55
56    pub fn add_filter(&mut self, filter: FileFilter) {
57        self.filters.push(filter);
58    }
59
60    pub fn open(
61        self,
62        parent: Option<impl AsWindow>,
63    ) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
64        filebox(
65            parent,
66            self.title,
67            self.filename,
68            self.filters,
69            true,
70            false,
71            false,
72        )
73        .map(|fut| fut.map(|res| res?.result()))
74    }
75
76    pub fn open_multiple(
77        self,
78        parent: Option<impl AsWindow>,
79    ) -> Result<impl Future<Output = Result<Vec<PathBuf>>> + 'static> {
80        filebox(
81            parent,
82            self.title,
83            self.filename,
84            self.filters,
85            true,
86            true,
87            false,
88        )
89        .map(|fut| fut.map(|res| res?.results()))
90    }
91
92    pub fn open_folder(
93        self,
94        parent: Option<impl AsWindow>,
95    ) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
96        filebox(
97            parent,
98            self.title,
99            self.filename,
100            self.filters,
101            true,
102            false,
103            true,
104        )
105        .map(|fut| fut.map(|res| res?.result()))
106    }
107
108    pub fn save(
109        self,
110        parent: Option<impl AsWindow>,
111    ) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
112        filebox(
113            parent,
114            self.title,
115            self.filename,
116            self.filters,
117            false,
118            false,
119            false,
120        )
121        .map(|fut| fut.map(|res| res?.result()))
122    }
123}
124
125fn filebox(
126    parent: Option<impl AsWindow>,
127    title: Retained<NSString>,
128    filename: Retained<NSString>,
129    filters: Vec<FileFilter>,
130    open: bool,
131    multiple: bool,
132    folder: bool,
133) -> Result<impl Future<Output = Result<FileBoxInner>> + 'static> {
134    let parent = parent.as_ref().map(|p| p.as_window().as_app_kit());
135    let mtm = parent
136        .as_ref()
137        .map(|w| w.mtm())
138        .or_else(MainThreadMarker::new)
139        .ok_or(Error::NotMainThread)?;
140
141    let handle = catch(|| {
142        let handle: Retained<NSSavePanel> = if open {
143            let handle = NSOpenPanel::openPanel(mtm);
144            handle.setCanChooseFiles(!folder);
145            handle.setCanChooseDirectories(folder);
146            handle.setResolvesAliases(false);
147            if multiple {
148                handle.setAllowsMultipleSelection(true);
149            }
150            Retained::into_super(handle)
151        } else {
152            let handle = NSSavePanel::savePanel(mtm);
153            handle.setCanCreateDirectories(true);
154            handle
155        };
156        handle.setShowsHiddenFiles(true);
157        handle.setExtensionHidden(false);
158        handle.setCanSelectHiddenExtension(false);
159        handle.setTreatsFilePackagesAsDirectories(true);
160
161        if let Some(parent) = &parent {
162            unsafe { handle.setParentWindow(Some(parent)) };
163        }
164
165        if !title.is_empty() {
166            handle.setTitle(Some(&title));
167        }
168
169        handle.setNameFieldStringValue(&filename);
170        if !filters.is_empty() {
171            let allow_others = filters
172                .iter()
173                .any(|f| f.pattern == "*.*" || f.pattern == "*");
174            handle.setAllowsOtherFileTypes(allow_others);
175
176            if !(open && allow_others) {
177                let ns_filters = NSArray::from_retained_slice(
178                    &filters
179                        .into_iter()
180                        .filter_map(|f| {
181                            let pattern = f.pattern;
182                            if pattern == "*.*" || pattern == "*" {
183                                None
184                            } else {
185                                UTType::typeWithFilenameExtension(&NSString::from_str(
186                                    pattern.strip_prefix("*.").unwrap_or(&pattern),
187                                ))
188                            }
189                        })
190                        .collect::<Vec<_>>(),
191                );
192                if !ns_filters.is_empty() {
193                    handle.setAllowedContentTypes(&ns_filters);
194                }
195            }
196        }
197        handle
198    })?;
199
200    let fut = if let Some(parent) = &parent {
201        let (tx, rx) = local_sync::oneshot::channel();
202        let tx = Rc::new(Cell::new(Some(tx)));
203        let block = StackBlock::new(move |res| {
204            tx.take()
205                .expect("the handler should only be called once")
206                .send(res)
207                .ok();
208        });
209        catch(|| handle.beginSheetModalForWindow_completionHandler(parent, &block))?;
210        Either::Left(rx.map_err(|e| e.into()))
211    } else {
212        Either::Right(std::future::ready(catch(|| handle.runModal())))
213    };
214    Ok(fut.map_ok(move |res| {
215        handle.close();
216        FileBoxInner(if res == NSModalResponseOK {
217            Some(handle)
218        } else {
219            None
220        })
221    }))
222}
223
224struct FileBoxInner(Option<Retained<NSSavePanel>>);
225
226impl FileBoxInner {
227    pub fn result(self) -> Result<Option<PathBuf>> {
228        if let Some(dialog) = self.0 {
229            catch(|| {
230                dialog
231                    .URL()
232                    .and_then(|url| url.path())
233                    .map(|s| PathBuf::from(from_nsstring(&s)))
234            })
235        } else {
236            Ok(None)
237        }
238    }
239
240    pub fn results(self) -> Result<Vec<PathBuf>> {
241        if let Some(dialog) = self.0 {
242            let dialog: Retained<NSOpenPanel> = unsafe { Retained::cast_unchecked(dialog) };
243            catch(|| {
244                dialog
245                    .URLs()
246                    .iter()
247                    .filter_map(|url| url.path().map(|s| PathBuf::from(from_nsstring(&s))))
248                    .collect()
249            })
250        } else {
251            Ok(vec![])
252        }
253    }
254}