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 PolyMeilex#36
  • Loading branch information
Be-ing committed Jan 18, 2022
1 parent 1ea9517 commit 404e88e
Show file tree
Hide file tree
Showing 4 changed files with 333 additions and 6 deletions.
13 changes: 8 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,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"
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
274 changes: 274 additions & 0 deletions src/backend/xdg_desktop_portal.rs
Original file line number Diff line number Diff line change
@@ -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<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_path(uri: &str) -> Option<PathBuf> {
uri.strip_prefix("file://").map(PathBuf::from)
}

//
// File Picker
//

use crate::backend::FilePickerDialogImpl;
impl FilePickerDialogImpl for FileDialog {
fn pick_file(self) -> Option<PathBuf> {
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<Vec<PathBuf>> {
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::<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 = 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<Option<Vec<FileHandle>>> {
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::<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 = 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<Option<FileHandle>> {
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<PathBuf> {
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<Option<FileHandle>> {
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)
})
}
}
30 changes: 30 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

0 comments on commit 404e88e

Please sign in to comment.