diff --git a/Cargo.toml b/Cargo.toml index ab019ab..1b98c8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ documentation = "https://docs.rs/rfd" default = ["parent"] parent = ["raw-window-handle"] file-handle-inner = [] +gtk3 = ["gtk-sys", "glib-sys", "gobject-sys", "lazy_static"] [dev-dependencies] futures = "0.3.12" @@ -35,11 +36,13 @@ windows = { version = "0.30.0", features = [ "Win32_UI_WindowsAndMessaging", ] } -[target.'cfg(any(target_os = "freebsd", target_os = "linux"))'.dependencies] -gtk-sys = { version = "0.15.1", features = ["v3_20"] } -glib-sys = "0.15.1" -gobject-sys = "0.15.1" -lazy_static = "1.4.0" +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +ashpd = "0.2.0-beta-1" +smol = "1.2" +gtk-sys = { version = "0.15.1", features = ["v3_20"], optional = true } +glib-sys = { version = "0.15.1", optional = true } +gobject-sys = { version = "0.15.1", optional = true } +lazy_static = { version = "1.4.0", optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = "0.2.69" diff --git a/src/backend.rs b/src/backend.rs index 79ff28d..9ae3c1e 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -3,7 +3,16 @@ use std::future::Future; use std::path::PathBuf; use std::pin::Pin; -#[cfg(any(target_os = "freebsd", target_os = "linux"))] +#[cfg(all( + any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk3" +))] mod gtk3; #[cfg(target_os = "macos")] mod macos; @@ -11,6 +20,17 @@ mod macos; mod wasm; #[cfg(target_os = "windows")] mod win_cid; +#[cfg(all( + any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd" + ), + not(feature = "gtk3") +))] +mod xdg_desktop_portal; // // Sync diff --git a/src/backend/xdg_desktop_portal.rs b/src/backend/xdg_desktop_portal.rs new file mode 100644 index 0000000..0425185 --- /dev/null +++ b/src/backend/xdg_desktop_portal.rs @@ -0,0 +1,274 @@ +use std::path::PathBuf; + +use crate::backend::DialogFutureType; +use crate::file_dialog::Filter; +use crate::{FileDialog, FileHandle}; + +use ashpd::desktop::file_chooser::{ + FileChooserProxy, FileFilter, OpenFileOptions, SaveFileOptions, +}; +// TODO: convert raw_window_handle::RawWindowHandle to ashpd::WindowIdentifier +use ashpd::{zbus, WindowIdentifier}; + +use smol::block_on; + +// +// Utility functions +// + +fn add_filters_to_open_file_options( + filters: Vec, + mut options: OpenFileOptions, +) -> OpenFileOptions { + for filter in &filters { + let mut ashpd_filter = FileFilter::new(&filter.name); + for file_extension in &filter.extensions { + ashpd_filter = ashpd_filter.glob(&format!("*.{}", file_extension)); + } + options = options.add_filter(ashpd_filter); + } + options +} + +fn add_filters_to_save_file_options( + filters: Vec, + mut options: SaveFileOptions, +) -> SaveFileOptions { + for filter in &filters { + let mut ashpd_filter = FileFilter::new(&filter.name); + for file_extension in &filter.extensions { + ashpd_filter = ashpd_filter.glob(&format!("*.{}", file_extension)); + } + options = options.add_filter(ashpd_filter); + } + options +} + +// refer to https://github.com/flatpak/xdg-desktop-portal/issues/213 +fn uri_to_path(uri: &str) -> Option { + uri.strip_prefix("file://").map(PathBuf::from) +} + +// +// File Picker +// + +use crate::backend::FilePickerDialogImpl; +impl FilePickerDialogImpl for FileDialog { + fn pick_file(self) -> Option { + let connection = block_on(zbus::Connection::session()).unwrap(); + let proxy = block_on(FileChooserProxy::new(&connection)).unwrap(); + let mut options = OpenFileOptions::default() + .accept_label("Pick file") + .multiple(false); + options = add_filters_to_open_file_options(self.filters, options); + let selected_files = block_on(proxy.open_file( + &WindowIdentifier::default(), + &self.title.unwrap_or_else(|| "Pick a file".to_string()), + options, + )); + if selected_files.is_err() { + return None; + } + uri_to_path(&selected_files.unwrap().uris()[0]) + } + + fn pick_files(self) -> Option> { + let connection = block_on(zbus::Connection::session()).unwrap(); + let proxy = block_on(FileChooserProxy::new(&connection)).unwrap(); + let mut options = OpenFileOptions::default() + .accept_label("Pick file") + .multiple(true); + options = add_filters_to_open_file_options(self.filters, options); + let selected_files = block_on(proxy.open_file( + &WindowIdentifier::default(), + &self.title.unwrap_or_else(|| "Pick a file".to_string()), + options, + )); + if selected_files.is_err() { + return None; + } + let selected_files = selected_files + .unwrap() + .uris() + .iter() + .filter_map(|string| uri_to_path(string)) + .collect::>(); + if selected_files.is_empty() { + return None; + } + Some(selected_files) + } +} + +use crate::backend::AsyncFilePickerDialogImpl; +impl AsyncFilePickerDialogImpl for FileDialog { + fn pick_file_async(self) -> DialogFutureType> { + Box::pin(async { + let connection = zbus::Connection::session().await.unwrap(); + let proxy = FileChooserProxy::new(&connection).await.unwrap(); + let mut options = OpenFileOptions::default() + .accept_label("Pick file") + .multiple(false); + options = add_filters_to_open_file_options(self.filters, options); + let selected_files = proxy + .open_file( + &WindowIdentifier::default(), + &self.title.unwrap_or_else(|| "Pick a file".to_string()), + options, + ) + .await; + if selected_files.is_err() { + return None; + } + uri_to_path(&selected_files.unwrap().uris()[0]).map(FileHandle::from) + }) + } + + fn pick_files_async(self) -> DialogFutureType>> { + Box::pin(async { + let connection = zbus::Connection::session().await.unwrap(); + let proxy = FileChooserProxy::new(&connection).await.unwrap(); + let mut options = OpenFileOptions::default() + .accept_label("Pick file(s)") + .multiple(true); + options = add_filters_to_open_file_options(self.filters, options); + let selected_files = proxy + .open_file( + &WindowIdentifier::default(), + &self + .title + .unwrap_or_else(|| "Pick one or more files".to_string()), + options, + ) + .await; + if selected_files.is_err() { + return None; + } + let selected_files = selected_files + .unwrap() + .uris() + .iter() + .filter_map(|string| uri_to_path(string)) + .map(FileHandle::from) + .collect::>(); + if selected_files.is_empty() { + return None; + } + Some(selected_files) + }) + } +} + +// +// Folder Picker +// + +use crate::backend::FolderPickerDialogImpl; +impl FolderPickerDialogImpl for FileDialog { + fn pick_folder(self) -> Option { + let connection = block_on(zbus::Connection::session()).unwrap(); + let proxy = block_on(FileChooserProxy::new(&connection)).unwrap(); + let mut options = OpenFileOptions::default() + .accept_label("Pick folder") + .multiple(false) + .directory(true); + options = add_filters_to_open_file_options(self.filters, options); + let selected_files = block_on(proxy + .open_file( + &WindowIdentifier::default(), + &self.title.unwrap_or_else(|| "Pick a folder".to_string()), + options, + )); + if selected_files.is_err() { + return None; + } + uri_to_path(&selected_files.unwrap().uris()[0]) + } +} + +use crate::backend::AsyncFolderPickerDialogImpl; +impl AsyncFolderPickerDialogImpl for FileDialog { + fn pick_folder_async(self) -> DialogFutureType> { + Box::pin(async { + let connection = zbus::Connection::session().await.unwrap(); + let proxy = FileChooserProxy::new(&connection).await.unwrap(); + let mut options = OpenFileOptions::default() + .accept_label("Pick folder") + .multiple(false) + .directory(true); + options = add_filters_to_open_file_options(self.filters, options); + let selected_files = proxy + .open_file( + &WindowIdentifier::default(), + &self.title.unwrap_or_else(|| "Pick a folder".to_string()), + options, + ) + .await; + if selected_files.is_err() { + return None; + } + uri_to_path(&selected_files.unwrap().uris()[0]).map(FileHandle::from) + }) + } +} + +// +// File Save +// + +use crate::backend::FileSaveDialogImpl; +impl FileSaveDialogImpl for FileDialog { + fn save_file(self) -> Option { + let connection = block_on(zbus::Connection::session()).unwrap(); + let proxy = block_on(FileChooserProxy::new(&connection)).unwrap(); + let mut options = SaveFileOptions::default().accept_label("Save"); + options = add_filters_to_save_file_options(self.filters, options); + if let Some(file_name) = self.file_name { + options = options.current_name(&file_name); + } + // TODO: impl zvariant::Type for PathBuf? + // if let Some(dir) = self.starting_directory { + // options.current_folder(dir); + // } + let selected_files = block_on(proxy.save_file( + &WindowIdentifier::default(), + &self.title.unwrap_or_else(|| "Save file".to_string()), + options, + )); + if selected_files.is_err() { + return None; + } + uri_to_path(&selected_files.unwrap().uris()[0]) + } +} + +use crate::backend::AsyncFileSaveDialogImpl; +impl AsyncFileSaveDialogImpl for FileDialog { + fn save_file_async(self) -> DialogFutureType> { + Box::pin(async { + let connection = zbus::Connection::session().await.unwrap(); + let proxy = FileChooserProxy::new(&connection).await.unwrap(); + let mut options = SaveFileOptions::default().accept_label("Save"); + options = add_filters_to_save_file_options(self.filters, options); + if let Some(file_name) = self.file_name { + options = options.current_name(&file_name); + } + // TODO: impl zvariant::Type for PathBuf? + // if let Some(dir) = self.starting_directory { + // options.current_folder(dir); + // } + let selected_files = proxy + .save_file( + &WindowIdentifier::default(), + &self.title.unwrap_or_else(|| "Save file".to_string()), + options, + ) + .await; + if selected_files.is_err() { + return None; + } + uri_to_path(&selected_files.unwrap().uris()[0]).map(FileHandle::from) + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index ed21e20..359e9f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,36 @@ pub use file_dialog::FileDialog; pub use file_dialog::AsyncFileDialog; +#[cfg(any( + target_os = "windows", + target_os = "macos", + target_family = "wasm", + all( + any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk3" + ) +))] mod message_dialog; +#[cfg(any( + target_os = "windows", + target_os = "macos", + target_family = "wasm", + all( + any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk3" + ) +))] pub use message_dialog::{AsyncMessageDialog, MessageButtons, MessageDialog, MessageLevel};