1use std::cell::Cell;
5
6use block2::RcBlock;
7use objc2::rc::Retained;
8use objc2::{MainThreadMarker, Message};
9use objc2_app_kit::{NSApplication, NSModalResponse, NSModalResponseOK, NSOpenPanel, NSSavePanel};
10use objc2_foundation::{NSArray, NSString, NSURL};
11use objc2_uniform_type_identifiers::UTType;
12
13use nifty_runtime::{DialogHandler, DialogKind, FileDialog, Picked};
14
15enum Panel {
17 Open(Retained<NSOpenPanel>),
18 Save(Retained<NSSavePanel>),
19}
20
21impl Panel {
22 fn new(mtm: MainThreadMarker, kind: DialogKind) -> Self {
23 if kind == DialogKind::SavePath {
24 return Self::Save(NSSavePanel::savePanel(mtm));
25 }
26 let panel = NSOpenPanel::openPanel(mtm);
27 panel.setCanChooseFiles(kind != DialogKind::Folder);
28 panel.setCanChooseDirectories(kind == DialogKind::Folder);
29 panel.setAllowsMultipleSelection(kind.picks_many());
30 Self::Open(panel)
31 }
32
33 fn save_panel(&self) -> &NSSavePanel {
34 match self {
35 Self::Open(panel) => panel,
36 Self::Save(panel) => panel,
37 }
38 }
39
40 fn chosen(&self) -> Result<Vec<String>, String> {
42 let urls = match self {
43 Self::Open(panel) => panel.URLs().to_vec(),
44 Self::Save(panel) => panel.URL().into_iter().collect(),
45 };
46 if urls.is_empty() {
47 return Err("macOS said a file was chosen but didn't say which".to_owned());
48 }
49 urls.iter().map(|url| path(url)).collect()
50 }
51}
52
53pub(crate) fn show(
55 mtm: MainThreadMarker,
56 app: &NSApplication,
57 dialog: &FileDialog,
58 on_close: DialogHandler,
59) {
60 let panel = Panel::new(mtm, dialog.kind);
61 if let Err(reason) = configure(panel.save_panel(), dialog) {
62 on_close(Picked::Failed(reason));
63 return;
64 }
65 let pending = Cell::new(Some(on_close));
66 let shown = panel.save_panel().retain();
67 let handler = RcBlock::new(move |response: NSModalResponse| {
68 let picked = if response == NSModalResponseOK {
69 panel.chosen().map_or_else(Picked::Failed, Picked::Chosen)
70 } else {
71 Picked::Cancelled
72 };
73 if let Some(on_close) = pending.take() {
74 on_close(picked);
75 }
76 });
77 match app.keyWindow() {
78 Some(window) => shown.beginSheetModalForWindow_completionHandler(&window, &handler),
79 None => shown.beginWithCompletionHandler(&handler),
80 }
81}
82
83fn configure(panel: &NSSavePanel, dialog: &FileDialog) -> Result<(), String> {
84 panel.setCanCreateDirectories(true);
85 if !dialog.title.is_empty() {
86 panel.setMessage(Some(&NSString::from_str(&dialog.title)));
88 }
89 if !dialog.types.is_empty() {
90 let types = dialog
91 .types
92 .iter()
93 .map(|extension| {
94 UTType::typeWithFilenameExtension(&NSString::from_str(extension))
95 .ok_or_else(|| format!("macOS doesn't know the file type `{extension}`"))
96 })
97 .collect::<Result<Vec<_>, _>>()?;
98 panel.setAllowedContentTypes(&NSArray::from_retained_slice(&types));
99 }
100 if let Some(folder) = &dialog.folder {
101 let folder = NSString::from_str(&folder.to_string_lossy());
102 panel.setDirectoryURL(Some(&NSURL::fileURLWithPath_isDirectory(&folder, true)));
103 }
104 if !dialog.name.is_empty() {
105 panel.setNameFieldStringValue(&NSString::from_str(&dialog.name));
106 }
107 Ok(())
108}
109
110fn path(url: &NSURL) -> Result<String, String> {
111 url.path()
112 .map(|path| path.to_string())
113 .ok_or_else(|| "macOS chose a location that isn't a file path".to_owned())
114}