use std::{cell::Cell, path::PathBuf, rc::Rc};
use block2::StackBlock;
use futures_util::{FutureExt, TryFutureExt, future::Either};
use objc2::{MainThreadOnly, rc::Retained};
use objc2_app_kit::{NSModalResponseOK, NSOpenPanel, NSSavePanel};
use objc2_foundation::{MainThreadMarker, NSArray, NSString};
use objc2_uniform_type_identifiers::UTType;
use winio_handle::AsWindow;
use crate::{Error, Result, catch, from_nsstring};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileFilter {
name: String,
pattern: String,
}
impl FileFilter {
pub fn new(name: &str, pattern: &str) -> Self {
Self {
name: name.to_string(),
pattern: pattern.to_string(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct FileBox {
title: Retained<NSString>,
filename: Retained<NSString>,
filters: Vec<FileFilter>,
}
unsafe impl Send for FileBox {}
unsafe impl Sync for FileBox {}
impl FileBox {
pub fn new() -> Self {
Self::default()
}
pub fn title(&mut self, title: &str) {
self.title = NSString::from_str(title);
}
pub fn filename(&mut self, filename: &str) {
self.filename = NSString::from_str(filename);
}
pub fn filters(&mut self, filters: impl IntoIterator<Item = FileFilter>) {
self.filters = filters.into_iter().collect();
}
pub fn add_filter(&mut self, filter: FileFilter) {
self.filters.push(filter);
}
pub fn open(
self,
parent: Option<impl AsWindow>,
) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
filebox(
parent,
self.title,
self.filename,
self.filters,
true,
false,
false,
)
.map(|fut| fut.map(|res| res?.result()))
}
pub fn open_multiple(
self,
parent: Option<impl AsWindow>,
) -> Result<impl Future<Output = Result<Vec<PathBuf>>> + 'static> {
filebox(
parent,
self.title,
self.filename,
self.filters,
true,
true,
false,
)
.map(|fut| fut.map(|res| res?.results()))
}
pub fn open_folder(
self,
parent: Option<impl AsWindow>,
) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
filebox(
parent,
self.title,
self.filename,
self.filters,
true,
false,
true,
)
.map(|fut| fut.map(|res| res?.result()))
}
pub fn save(
self,
parent: Option<impl AsWindow>,
) -> Result<impl Future<Output = Result<Option<PathBuf>>> + 'static> {
filebox(
parent,
self.title,
self.filename,
self.filters,
false,
false,
false,
)
.map(|fut| fut.map(|res| res?.result()))
}
}
fn filebox(
parent: Option<impl AsWindow>,
title: Retained<NSString>,
filename: Retained<NSString>,
filters: Vec<FileFilter>,
open: bool,
multiple: bool,
folder: bool,
) -> Result<impl Future<Output = Result<FileBoxInner>> + 'static> {
let parent = parent.as_ref().map(|p| p.as_window().as_app_kit());
let mtm = parent
.as_ref()
.map(|w| w.mtm())
.or_else(MainThreadMarker::new)
.ok_or(Error::NotMainThread)?;
let handle = catch(|| {
let handle: Retained<NSSavePanel> = if open {
let handle = NSOpenPanel::openPanel(mtm);
handle.setCanChooseFiles(!folder);
handle.setCanChooseDirectories(folder);
handle.setResolvesAliases(false);
if multiple {
handle.setAllowsMultipleSelection(true);
}
Retained::into_super(handle)
} else {
let handle = NSSavePanel::savePanel(mtm);
handle.setCanCreateDirectories(true);
handle
};
handle.setShowsHiddenFiles(true);
handle.setExtensionHidden(false);
handle.setCanSelectHiddenExtension(false);
handle.setTreatsFilePackagesAsDirectories(true);
if let Some(parent) = &parent {
unsafe { handle.setParentWindow(Some(parent)) };
}
if !title.is_empty() {
handle.setTitle(Some(&title));
}
handle.setNameFieldStringValue(&filename);
if !filters.is_empty() {
let allow_others = filters
.iter()
.any(|f| f.pattern == "*.*" || f.pattern == "*");
handle.setAllowsOtherFileTypes(allow_others);
if !(open && allow_others) {
let ns_filters = NSArray::from_retained_slice(
&filters
.into_iter()
.filter_map(|f| {
let pattern = f.pattern;
if pattern == "*.*" || pattern == "*" {
None
} else {
UTType::typeWithFilenameExtension(&NSString::from_str(
pattern.strip_prefix("*.").unwrap_or(&pattern),
))
}
})
.collect::<Vec<_>>(),
);
if !ns_filters.is_empty() {
handle.setAllowedContentTypes(&ns_filters);
}
}
}
handle
})?;
let fut = if let Some(parent) = &parent {
let (tx, rx) = local_sync::oneshot::channel();
let tx = Rc::new(Cell::new(Some(tx)));
let block = StackBlock::new(move |res| {
tx.take()
.expect("the handler should only be called once")
.send(res)
.ok();
});
catch(|| handle.beginSheetModalForWindow_completionHandler(parent, &block))?;
Either::Left(rx.map_err(|e| e.into()))
} else {
Either::Right(std::future::ready(catch(|| handle.runModal())))
};
Ok(fut.map_ok(move |res| {
handle.close();
FileBoxInner(if res == NSModalResponseOK {
Some(handle)
} else {
None
})
}))
}
struct FileBoxInner(Option<Retained<NSSavePanel>>);
impl FileBoxInner {
pub fn result(self) -> Result<Option<PathBuf>> {
if let Some(dialog) = self.0 {
catch(|| {
dialog
.URL()
.and_then(|url| url.path())
.map(|s| PathBuf::from(from_nsstring(&s)))
})
} else {
Ok(None)
}
}
pub fn results(self) -> Result<Vec<PathBuf>> {
if let Some(dialog) = self.0 {
let dialog: Retained<NSOpenPanel> = unsafe { Retained::cast_unchecked(dialog) };
catch(|| {
dialog
.URLs()
.iter()
.filter_map(|url| url.path().map(|s| PathBuf::from(from_nsstring(&s))))
.collect()
})
} else {
Ok(vec![])
}
}
}