Skip to content

Commit

Permalink
X11 dialogs, take 2. (linebender#2153)
Browse files Browse the repository at this point in the history
Use xdg-desktop-portal's dbus APIs for open/save dialogs on x11.
  • Loading branch information
jneem authored and xarvic committed Jul 29, 2022
1 parent 4dda27a commit 8ca0466
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 18 deletions.
14 changes: 12 additions & 2 deletions druid-shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ default-target = "x86_64-pc-windows-msvc"
[features]
default = ["gtk"]
gtk = ["gdk-sys", "glib-sys", "gtk-sys", "gtk-rs"]
x11 = ["x11rb", "nix", "cairo-sys-rs", "bindgen", "pkg-config"]
x11 = [
"ashpd",
"bindgen",
"cairo-sys-rs",
"futures",
"nix",
"pkg-config",
"x11rb",
]
wayland = [
"wayland-client",
"wayland-protocols/client",
Expand Down Expand Up @@ -68,7 +76,7 @@ keyboard-types = { version = "0.6.2", default_features = false }

# Optional dependencies
image = { version = "0.23.12", optional = true, default_features = false }
raw-window-handle = { version = "0.3.3", optional = true, default_features = false }
raw-window-handle = { version = "0.4.2", optional = true, default_features = false }

[target.'cfg(target_os="windows")'.dependencies]
scopeguard = "1.1.0"
Expand All @@ -90,9 +98,11 @@ foreign-types = "0.3.2"
bitflags = "1.2.1"

[target.'cfg(any(target_os="linux", target_os="openbsd"))'.dependencies]
ashpd = { version = "0.3.0", optional = true }
# TODO(x11/dependencies): only use feature "xcb" if using X11
cairo-rs = { version = "0.14.0", default_features = false, features = ["xcb"] }
cairo-sys-rs = { version = "0.14.0", default_features = false, optional = true }
futures = { version = "0.3.21", optional = true, features = ["executor"]}
gdk-sys = { version = "0.14.0", optional = true }
# `gtk` gets renamed to `gtk-rs` so that we can use `gtk` as the feature name.
gtk-rs = { version = "0.14.0", features = ["v3_22"], package = "gtk", optional = true }
Expand Down
166 changes: 166 additions & 0 deletions druid-shell/src/backend/x11/dialog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! This module contains functions for opening file dialogs using DBus.

use ashpd::desktop::file_chooser;
use ashpd::{zbus, WindowIdentifier};
use futures::executor::block_on;
use tracing::warn;

use crate::{FileDialogOptions, FileDialogToken, FileInfo};

use super::window::IdleHandle;

pub(crate) fn open_file(
window: u32,
idle: IdleHandle,
options: FileDialogOptions,
) -> FileDialogToken {
dialog(window, idle, options, true)
}

pub(crate) fn save_file(
window: u32,
idle: IdleHandle,
options: FileDialogOptions,
) -> FileDialogToken {
dialog(window, idle, options, false)
}

fn dialog(
window: u32,
idle: IdleHandle,
mut options: FileDialogOptions,
open: bool,
) -> FileDialogToken {
let tok = FileDialogToken::next();

std::thread::spawn(move || {
if let Err(e) = block_on(async {
let conn = zbus::Connection::session().await?;
let proxy = file_chooser::FileChooserProxy::new(&conn).await?;
let id = WindowIdentifier::from_xid(window as u64);
let multi = options.multi_selection;

let title_owned = options.title.take();
let title = match (open, options.select_directories) {
(true, true) => "Open Folder",
(true, false) => "Open File",
(false, _) => "Save File",
};
let title = title_owned.as_deref().unwrap_or(title);
let open_result;
let save_result;
let uris = if open {
open_result = proxy.open_file(&id, title, options.into()).await?;
open_result.uris()
} else {
save_result = proxy.save_file(&id, title, options.into()).await?;
save_result.uris()
};

let mut paths = uris.iter().filter_map(|s| {
s.strip_prefix("file://").or_else(|| {
warn!("expected path '{}' to start with 'file://'", s);
None
})
});
if multi && open {
let infos = paths
.map(|p| FileInfo {
path: p.into(),
format: None,
})
.collect();
idle.add_idle_callback(move |handler| handler.open_files(tok, infos));
} else if !multi {
if uris.len() > 2 {
warn!(
"expected one path (got {}), returning only the first",
uris.len()
);
}
let info = paths.next().map(|p| FileInfo {
path: p.into(),
format: None,
});
if open {
idle.add_idle_callback(move |handler| handler.open_file(tok, info));
} else {
idle.add_idle_callback(move |handler| handler.save_as(tok, info));
}
} else {
warn!("cannot save multiple paths");
}

Ok(()) as ashpd::Result<()>
}) {
warn!("error while opening file dialog: {}", e);
}
});

tok
}

impl From<crate::FileSpec> for file_chooser::FileFilter {
fn from(spec: crate::FileSpec) -> file_chooser::FileFilter {
let mut filter = file_chooser::FileFilter::new(spec.name);
for ext in spec.extensions {
filter = filter.glob(&format!("*.{}", ext));
}
filter
}
}

impl From<crate::FileDialogOptions> for file_chooser::OpenFileOptions {
fn from(opts: crate::FileDialogOptions) -> file_chooser::OpenFileOptions {
let mut fc = file_chooser::OpenFileOptions::default()
.modal(true)
.multiple(opts.multi_selection)
.directory(opts.select_directories);

if let Some(label) = &opts.button_text {
fc = fc.accept_label(label);
}

if let Some(filters) = opts.allowed_types {
for f in filters {
fc = fc.add_filter(f.into());
}
}

if let Some(filter) = opts.default_type {
fc = fc.current_filter(filter.into());
}

fc
}
}

impl From<crate::FileDialogOptions> for file_chooser::SaveFileOptions {
fn from(opts: crate::FileDialogOptions) -> file_chooser::SaveFileOptions {
let mut fc = file_chooser::SaveFileOptions::default().modal(true);

if let Some(name) = &opts.default_name {
fc = fc.current_name(name);
}

if let Some(label) = &opts.button_text {
fc = fc.accept_label(label);
}

if let Some(filters) = opts.allowed_types {
for f in filters {
fc = fc.add_filter(f.into());
}
}

if let Some(filter) = opts.default_type {
fc = fc.current_filter(filter.into());
}

if let Some(dir) = &opts.starting_directory {
fc = fc.current_folder(dir);
}

fc
}
}
1 change: 1 addition & 0 deletions druid-shell/src/backend/x11/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ mod util;

pub mod application;
pub mod clipboard;
pub mod dialog;
pub mod error;
pub mod menu;
pub mod screen;
Expand Down
46 changes: 30 additions & 16 deletions druid-shell/src/backend/x11/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ use crate::window::{
use crate::{window, KeyEvent, ScaledArea};

use super::application::Application;
use super::dialog;
use super::menu::Menu;

/// A version of XCB's `xcb_visualtype_t` struct. This was copied from the [example] in x11rb; it
Expand Down Expand Up @@ -1557,23 +1558,22 @@ impl IdleHandle {
}

pub(crate) fn schedule_redraw(&self) {
self.queue.lock().unwrap().push(IdleKind::Redraw);
self.wake();
self.add_idle(IdleKind::Redraw);
}

pub fn add_idle_callback<F>(&self, callback: F)
where
F: FnOnce(&mut dyn WinHandler) + Send + 'static,
{
self.queue
.lock()
.unwrap()
.push(IdleKind::Callback(Box::new(callback)));
self.wake();
self.add_idle(IdleKind::Callback(Box::new(callback)));
}

pub fn add_idle_token(&self, token: IdleToken) {
self.queue.lock().unwrap().push(IdleKind::Token(token));
self.add_idle(IdleKind::Token(token));
}

fn add_idle(&self, idle: IdleKind) {
self.queue.lock().unwrap().push(idle);
self.wake();
}
}
Expand Down Expand Up @@ -1795,16 +1795,30 @@ impl WindowHandle {
}
}

pub fn open_file(&mut self, _options: FileDialogOptions) -> Option<FileDialogToken> {
// TODO(x11/file_dialogs): implement WindowHandle::open_file
warn!("WindowHandle::open_file is currently unimplemented for X11 backend.");
None
pub fn open_file(&mut self, options: FileDialogOptions) -> Option<FileDialogToken> {
if let Some(w) = self.window.upgrade() {
if let Some(idle) = self.get_idle_handle() {
Some(dialog::open_file(w.id, idle, options))
} else {
warn!("Couldn't open file because no idle handle available");
None
}
} else {
None
}
}

pub fn save_as(&mut self, _options: FileDialogOptions) -> Option<FileDialogToken> {
// TODO(x11/file_dialogs): implement WindowHandle::save_as
warn!("WindowHandle::save_as is currently unimplemented for X11 backend.");
None
pub fn save_as(&mut self, options: FileDialogOptions) -> Option<FileDialogToken> {
if let Some(w) = self.window.upgrade() {
if let Some(idle) = self.get_idle_handle() {
Some(dialog::save_file(w.id, idle, options))
} else {
warn!("Couldn't save file because no idle handle available");
None
}
} else {
None
}
}

pub fn show_context_menu(&self, _menu: Menu, _pos: Point) {
Expand Down

0 comments on commit 8ca0466

Please sign in to comment.