winio_ui_app_kit/dialogs/
filebox.rs1use 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
35unsafe 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}