From e331eff49a95b095e2e82750af114cf0c81c3dfd Mon Sep 17 00:00:00 2001 From: Be Date: Tue, 18 Jan 2022 16:47:58 -0600 Subject: [PATCH] Add suport for XDG Desktop Portal on Linux & BSDs (#41) * use XDG Desktop Portal on Linux & BSDs This new backend does not support MessageDialog nor AsyncMessageDialog because there is no corresponding API in the XDG Desktop Portal. The GTK backend is still available with the new `gtk3` Cargo feature. Fixes #36 * replace smol with pollster pollster is smaller than smol * rename unwrap_or_warn to ok_or_warn * reuse async functions to implement sync functions * replace Option::ok with ok_or_warn * impl From for PathBuf to reduce code duplication * cargo fmt * factor out file_chooser_proxy function * add link to ashpd issue for RawWindowHandle https://github.com/bilelmoussaoui/ashpd/issues/40 * make gtk3 a default feature --- Cargo.toml | 16 ++- examples/msg.rs | 15 +++ src/backend.rs | 22 ++- src/backend/xdg_desktop_portal.rs | 216 ++++++++++++++++++++++++++++++ src/file_handle/native.rs | 12 ++ src/lib.rs | 30 +++++ 6 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 src/backend/xdg_desktop_portal.rs diff --git a/Cargo.toml b/Cargo.toml index ab019ab..daaaec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,10 @@ repository = "https://github.com/PolyMeilex/rfd" documentation = "https://docs.rs/rfd" [features] -default = ["parent"] +default = ["parent", "gtk3"] 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,14 @@ 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" +pollster = "0.2" +log = "0.4" +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/examples/msg.rs b/examples/msg.rs index d2e3136..4d1feb8 100644 --- a/examples/msg.rs +++ b/examples/msg.rs @@ -1,4 +1,19 @@ fn main() { + let res = ""; + #[cfg(any( + target_os = "windows", + target_os = "macos", + all( + any( + target_os = "linux", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk3" + ) + ))] let res = rfd::MessageDialog::new() .set_title("Msg!") .set_description("Description!") 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..f75b09b --- /dev/null +++ b/src/backend/xdg_desktop_portal.rs @@ -0,0 +1,216 @@ +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 +// https://github.com/bilelmoussaoui/ashpd/issues/40 +use ashpd::{zbus, WindowIdentifier}; + +use log::warn; +use pollster::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_pathbuf(uri: &str) -> Option { + uri.strip_prefix("file://").map(PathBuf::from) +} + +fn ok_or_warn(result: Result) -> Option { + match result { + Err(e) => { + warn!("{:?}", e); + None + } + Ok(t) => Some(t), + } +} + +async fn file_chooser_proxy<'a>() -> Option> { + let connection = ok_or_warn(zbus::Connection::session().await)?; + ok_or_warn(FileChooserProxy::new(&connection).await) +} + +// +// File Picker +// + +use crate::backend::FilePickerDialogImpl; +impl FilePickerDialogImpl for FileDialog { + fn pick_file(self) -> Option { + block_on(self.pick_file_async()).map(PathBuf::from) + } + + fn pick_files(self) -> Option> { + block_on(self.pick_files_async()) + .map(|vec_file_handle| vec_file_handle.iter().map(PathBuf::from).collect()) + } +} + +use crate::backend::AsyncFilePickerDialogImpl; +impl AsyncFilePickerDialogImpl for FileDialog { + fn pick_file_async(self) -> DialogFutureType> { + Box::pin(async { + let proxy = file_chooser_proxy().await?; + 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_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from) + }) + } + + fn pick_files_async(self) -> DialogFutureType>> { + Box::pin(async { + let proxy = file_chooser_proxy().await?; + 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_pathbuf(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 { + block_on(self.pick_folder_async()).map(PathBuf::from) + } +} + +use crate::backend::AsyncFolderPickerDialogImpl; +impl AsyncFolderPickerDialogImpl for FileDialog { + fn pick_folder_async(self) -> DialogFutureType> { + Box::pin(async { + let proxy = file_chooser_proxy().await?; + 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_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from) + }) + } +} + +// +// File Save +// + +use crate::backend::FileSaveDialogImpl; +impl FileSaveDialogImpl for FileDialog { + fn save_file(self) -> Option { + block_on(self.save_file_async()).map(PathBuf::from) + } +} + +use crate::backend::AsyncFileSaveDialogImpl; +impl AsyncFileSaveDialogImpl for FileDialog { + fn save_file_async(self) -> DialogFutureType> { + Box::pin(async { + let proxy = file_chooser_proxy().await?; + 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_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from) + }) + } +} diff --git a/src/file_handle/native.rs b/src/file_handle/native.rs index 2a34454..6340f21 100644 --- a/src/file_handle/native.rs +++ b/src/file_handle/native.rs @@ -119,3 +119,15 @@ impl From for FileHandle { Self(path) } } + +impl From for PathBuf { + fn from(file_handle: FileHandle) -> Self { + PathBuf::from(file_handle.path()) + } +} + +impl From<&FileHandle> for PathBuf { + fn from(file_handle: &FileHandle) -> Self { + PathBuf::from(file_handle.path()) + } +} 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};