Skip to content

Commit ab9a56a

Browse files
jneemxarvic
authored andcommitted
X11 dialogs, take 2. (linebender#2153)
Use xdg-desktop-portal's dbus APIs for open/save dialogs on x11.
1 parent d10e09b commit ab9a56a

File tree

4 files changed

+209
-18
lines changed

4 files changed

+209
-18
lines changed

druid-shell/Cargo.toml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ default-target = "x86_64-pc-windows-msvc"
1616
[features]
1717
default = ["gtk"]
1818
gtk = ["gdk-sys", "glib-sys", "gtk-sys", "gtk-rs"]
19-
x11 = ["x11rb", "nix", "cairo-sys-rs", "bindgen", "pkg-config"]
19+
x11 = [
20+
"ashpd",
21+
"bindgen",
22+
"cairo-sys-rs",
23+
"futures",
24+
"nix",
25+
"pkg-config",
26+
"x11rb",
27+
]
2028
wayland = [
2129
"wayland-client",
2230
"wayland-protocols/client",
@@ -68,7 +76,7 @@ keyboard-types = { version = "0.6.2", default_features = false }
6876

6977
# Optional dependencies
7078
image = { version = "0.23.12", optional = true, default_features = false }
71-
raw-window-handle = { version = "0.3.3", optional = true, default_features = false }
79+
raw-window-handle = { version = "0.4.2", optional = true, default_features = false }
7280

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

92100
[target.'cfg(any(target_os="linux", target_os="openbsd"))'.dependencies]
101+
ashpd = { version = "0.3.0", optional = true }
93102
# TODO(x11/dependencies): only use feature "xcb" if using X11
94103
cairo-rs = { version = "0.14.0", default_features = false, features = ["xcb"] }
95104
cairo-sys-rs = { version = "0.14.0", default_features = false, optional = true }
105+
futures = { version = "0.3.21", optional = true, features = ["executor"]}
96106
gdk-sys = { version = "0.14.0", optional = true }
97107
# `gtk` gets renamed to `gtk-rs` so that we can use `gtk` as the feature name.
98108
gtk-rs = { version = "0.14.0", features = ["v3_22"], package = "gtk", optional = true }
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//! This module contains functions for opening file dialogs using DBus.
2+
3+
use ashpd::desktop::file_chooser;
4+
use ashpd::{zbus, WindowIdentifier};
5+
use futures::executor::block_on;
6+
use tracing::warn;
7+
8+
use crate::{FileDialogOptions, FileDialogToken, FileInfo};
9+
10+
use super::window::IdleHandle;
11+
12+
pub(crate) fn open_file(
13+
window: u32,
14+
idle: IdleHandle,
15+
options: FileDialogOptions,
16+
) -> FileDialogToken {
17+
dialog(window, idle, options, true)
18+
}
19+
20+
pub(crate) fn save_file(
21+
window: u32,
22+
idle: IdleHandle,
23+
options: FileDialogOptions,
24+
) -> FileDialogToken {
25+
dialog(window, idle, options, false)
26+
}
27+
28+
fn dialog(
29+
window: u32,
30+
idle: IdleHandle,
31+
mut options: FileDialogOptions,
32+
open: bool,
33+
) -> FileDialogToken {
34+
let tok = FileDialogToken::next();
35+
36+
std::thread::spawn(move || {
37+
if let Err(e) = block_on(async {
38+
let conn = zbus::Connection::session().await?;
39+
let proxy = file_chooser::FileChooserProxy::new(&conn).await?;
40+
let id = WindowIdentifier::from_xid(window as u64);
41+
let multi = options.multi_selection;
42+
43+
let title_owned = options.title.take();
44+
let title = match (open, options.select_directories) {
45+
(true, true) => "Open Folder",
46+
(true, false) => "Open File",
47+
(false, _) => "Save File",
48+
};
49+
let title = title_owned.as_deref().unwrap_or(title);
50+
let open_result;
51+
let save_result;
52+
let uris = if open {
53+
open_result = proxy.open_file(&id, title, options.into()).await?;
54+
open_result.uris()
55+
} else {
56+
save_result = proxy.save_file(&id, title, options.into()).await?;
57+
save_result.uris()
58+
};
59+
60+
let mut paths = uris.iter().filter_map(|s| {
61+
s.strip_prefix("file://").or_else(|| {
62+
warn!("expected path '{}' to start with 'file://'", s);
63+
None
64+
})
65+
});
66+
if multi && open {
67+
let infos = paths
68+
.map(|p| FileInfo {
69+
path: p.into(),
70+
format: None,
71+
})
72+
.collect();
73+
idle.add_idle_callback(move |handler| handler.open_files(tok, infos));
74+
} else if !multi {
75+
if uris.len() > 2 {
76+
warn!(
77+
"expected one path (got {}), returning only the first",
78+
uris.len()
79+
);
80+
}
81+
let info = paths.next().map(|p| FileInfo {
82+
path: p.into(),
83+
format: None,
84+
});
85+
if open {
86+
idle.add_idle_callback(move |handler| handler.open_file(tok, info));
87+
} else {
88+
idle.add_idle_callback(move |handler| handler.save_as(tok, info));
89+
}
90+
} else {
91+
warn!("cannot save multiple paths");
92+
}
93+
94+
Ok(()) as ashpd::Result<()>
95+
}) {
96+
warn!("error while opening file dialog: {}", e);
97+
}
98+
});
99+
100+
tok
101+
}
102+
103+
impl From<crate::FileSpec> for file_chooser::FileFilter {
104+
fn from(spec: crate::FileSpec) -> file_chooser::FileFilter {
105+
let mut filter = file_chooser::FileFilter::new(spec.name);
106+
for ext in spec.extensions {
107+
filter = filter.glob(&format!("*.{}", ext));
108+
}
109+
filter
110+
}
111+
}
112+
113+
impl From<crate::FileDialogOptions> for file_chooser::OpenFileOptions {
114+
fn from(opts: crate::FileDialogOptions) -> file_chooser::OpenFileOptions {
115+
let mut fc = file_chooser::OpenFileOptions::default()
116+
.modal(true)
117+
.multiple(opts.multi_selection)
118+
.directory(opts.select_directories);
119+
120+
if let Some(label) = &opts.button_text {
121+
fc = fc.accept_label(label);
122+
}
123+
124+
if let Some(filters) = opts.allowed_types {
125+
for f in filters {
126+
fc = fc.add_filter(f.into());
127+
}
128+
}
129+
130+
if let Some(filter) = opts.default_type {
131+
fc = fc.current_filter(filter.into());
132+
}
133+
134+
fc
135+
}
136+
}
137+
138+
impl From<crate::FileDialogOptions> for file_chooser::SaveFileOptions {
139+
fn from(opts: crate::FileDialogOptions) -> file_chooser::SaveFileOptions {
140+
let mut fc = file_chooser::SaveFileOptions::default().modal(true);
141+
142+
if let Some(name) = &opts.default_name {
143+
fc = fc.current_name(name);
144+
}
145+
146+
if let Some(label) = &opts.button_text {
147+
fc = fc.accept_label(label);
148+
}
149+
150+
if let Some(filters) = opts.allowed_types {
151+
for f in filters {
152+
fc = fc.add_filter(f.into());
153+
}
154+
}
155+
156+
if let Some(filter) = opts.default_type {
157+
fc = fc.current_filter(filter.into());
158+
}
159+
160+
if let Some(dir) = &opts.starting_directory {
161+
fc = fc.current_folder(dir);
162+
}
163+
164+
fc
165+
}
166+
}

druid-shell/src/backend/x11/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ mod util;
3535

3636
pub mod application;
3737
pub mod clipboard;
38+
pub mod dialog;
3839
pub mod error;
3940
pub mod menu;
4041
pub mod screen;

druid-shell/src/backend/x11/window.rs

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ use crate::window::{
6161
use crate::{window, KeyEvent, ScaledArea};
6262

6363
use super::application::Application;
64+
use super::dialog;
6465
use super::menu::Menu;
6566

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

15591560
pub(crate) fn schedule_redraw(&self) {
1560-
self.queue.lock().unwrap().push(IdleKind::Redraw);
1561-
self.wake();
1561+
self.add_idle(IdleKind::Redraw);
15621562
}
15631563

15641564
pub fn add_idle_callback<F>(&self, callback: F)
15651565
where
15661566
F: FnOnce(&mut dyn WinHandler) + Send + 'static,
15671567
{
1568-
self.queue
1569-
.lock()
1570-
.unwrap()
1571-
.push(IdleKind::Callback(Box::new(callback)));
1572-
self.wake();
1568+
self.add_idle(IdleKind::Callback(Box::new(callback)));
15731569
}
15741570

15751571
pub fn add_idle_token(&self, token: IdleToken) {
1576-
self.queue.lock().unwrap().push(IdleKind::Token(token));
1572+
self.add_idle(IdleKind::Token(token));
1573+
}
1574+
1575+
fn add_idle(&self, idle: IdleKind) {
1576+
self.queue.lock().unwrap().push(idle);
15771577
self.wake();
15781578
}
15791579
}
@@ -1795,16 +1795,30 @@ impl WindowHandle {
17951795
}
17961796
}
17971797

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

1804-
pub fn save_as(&mut self, _options: FileDialogOptions) -> Option<FileDialogToken> {
1805-
// TODO(x11/file_dialogs): implement WindowHandle::save_as
1806-
warn!("WindowHandle::save_as is currently unimplemented for X11 backend.");
1807-
None
1811+
pub fn save_as(&mut self, options: FileDialogOptions) -> Option<FileDialogToken> {
1812+
if let Some(w) = self.window.upgrade() {
1813+
if let Some(idle) = self.get_idle_handle() {
1814+
Some(dialog::save_file(w.id, idle, options))
1815+
} else {
1816+
warn!("Couldn't save file because no idle handle available");
1817+
None
1818+
}
1819+
} else {
1820+
None
1821+
}
18081822
}
18091823

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

0 commit comments

Comments
 (0)