Skip to content

Commit

Permalink
use XDG Desktop Portal on Linux & BSDs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Be-ing committed Jan 18, 2022
1 parent 1ea9517 commit e46df9b
Show file tree
Hide file tree
Showing 5 changed files with 359 additions and 6 deletions.
14 changes: 9 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
smol = "1.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"
Expand Down
15 changes: 15 additions & 0 deletions examples/msg.rs
Original file line number Diff line number Diff line change
@@ -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!")
Expand Down
22 changes: 21 additions & 1 deletion src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,34 @@ 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;
#[cfg(target_arch = "wasm32")]
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
Expand Down
284 changes: 284 additions & 0 deletions src/backend/xdg_desktop_portal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
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 log::warn;
use smol::block_on;

//
// Utility functions
//

fn add_filters_to_open_file_options(
filters: Vec<Filter>,
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<Filter>,
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<PathBuf> {
uri.strip_prefix("file://").map(PathBuf::from)
}

fn unwrap_or_warn<T, E: std::fmt::Debug>(result: Result<T, E>) -> Option<T> {
match result {
Err(e) => {
warn!("{:?}", e);
None
}
Ok(t) => Some(t),
}
}

//
// File Picker
//

use crate::backend::FilePickerDialogImpl;
impl FilePickerDialogImpl for FileDialog {
fn pick_file(self) -> Option<PathBuf> {
let connection = unwrap_or_warn(block_on(zbus::Connection::session()))?;
let proxy = unwrap_or_warn(block_on(FileChooserProxy::new(&connection)))?;
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_pathbuf(&selected_files.unwrap().uris()[0])
}

fn pick_files(self) -> Option<Vec<PathBuf>> {
let connection = unwrap_or_warn(block_on(zbus::Connection::session()))?;
let proxy = unwrap_or_warn(block_on(FileChooserProxy::new(&connection)))?;
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_pathbuf(string))
.collect::<Vec<PathBuf>>();
if selected_files.is_empty() {
return None;
}
Some(selected_files)
}
}

use crate::backend::AsyncFilePickerDialogImpl;
impl AsyncFilePickerDialogImpl for FileDialog {
fn pick_file_async(self) -> DialogFutureType<Option<FileHandle>> {
Box::pin(async {
let connection = unwrap_or_warn(zbus::Connection::session().await)?;
let proxy = unwrap_or_warn(FileChooserProxy::new(&connection).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<Option<Vec<FileHandle>>> {
Box::pin(async {
let connection = unwrap_or_warn(zbus::Connection::session().await)?;
let proxy = unwrap_or_warn(FileChooserProxy::new(&connection).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::<Vec<FileHandle>>();
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<PathBuf> {
let connection = unwrap_or_warn(block_on(zbus::Connection::session()))?;
let proxy = unwrap_or_warn(block_on(FileChooserProxy::new(&connection)))?;
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_pathbuf(&selected_files.unwrap().uris()[0])
}
}

use crate::backend::AsyncFolderPickerDialogImpl;
impl AsyncFolderPickerDialogImpl for FileDialog {
fn pick_folder_async(self) -> DialogFutureType<Option<FileHandle>> {
Box::pin(async {
let connection = zbus::Connection::session().await.ok()?;
let proxy = FileChooserProxy::new(&connection).await.ok()?;
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<PathBuf> {
let connection = block_on(zbus::Connection::session()).ok()?;
let proxy = block_on(FileChooserProxy::new(&connection)).ok()?;
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_pathbuf(&selected_files.unwrap().uris()[0])
}
}

use crate::backend::AsyncFileSaveDialogImpl;
impl AsyncFileSaveDialogImpl for FileDialog {
fn save_file_async(self) -> DialogFutureType<Option<FileHandle>> {
Box::pin(async {
let connection = zbus::Connection::session().await.ok()?;
let proxy = FileChooserProxy::new(&connection).await.ok()?;
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)
})
}
}
Loading

0 comments on commit e46df9b

Please sign in to comment.