//! The freedesktop file chooser: `org.freedesktop.portal.FileChooser`. //! //! One of [`FileDialog`](super::FileDialog)'s platform backends, and the only //! one so far. It works under a sandbox, honours the user's bookmarks and //! settings, and is the dialog every other application on the desktop shows. use std::collections::HashMap; use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; /// Ask the portal for files, blocking until the user is done. pub(super) fn open_file( title: &str, multiple: bool, filters: &[(String, String)], ) -> Result, Box> { let connection = zbus::blocking::Connection::session()?; // The portal answers on a Request object whose path we can predict — so // we subscribe *before* asking, rather than racing a reply that could // arrive before we were listening. The spelling is the portal spec's: our // unique name without its leading colon and with dots as underscores, // plus a token we choose. let unique = connection .unique_name() .ok_or("the session bus gave us no unique name")? .to_string(); let sender = unique.trim_start_matches(':').replace('.', "_"); let token = format!("guiduck_{}", std::process::id()); let request_path = format!("/org/freedesktop/portal/desktop/request/{sender}/{token}"); let request = zbus::blocking::Proxy::new( &connection, "org.freedesktop.portal.Desktop", request_path.as_str(), "org.freedesktop.portal.Request", )?; let mut responses = request.receive_signal("Response")?; let mut options: HashMap<&str, Value<'_>> = HashMap::new(); options.insert("handle_token", Value::from(token.as_str())); options.insert("multiple", Value::from(multiple)); if !filters.is_empty() { // A filter is (name, [(kind, pattern)]); kind 0 is a glob. let filters: Vec<(String, Vec<(u32, String)>)> = filters .iter() .map(|(label, glob)| (label.clone(), vec![(0u32, glob.clone())])) .collect(); options.insert("filters", Value::from(filters)); } let chooser = zbus::blocking::Proxy::new( &connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.FileChooser", )?; // An empty parent: winit exposes no Wayland window handle we could name // one with (see `guiduck_core::widget::dialog`). The dialog is still real // and compositor-managed; it is just not marked as ours. let _handle: OwnedObjectPath = chooser.call("OpenFile", &("", title, options))?; // Blocks until the user is done. That is the whole reason for the thread. let message = responses .next() .ok_or("the portal closed without answering")?; let (response, results): (u32, HashMap) = message.body().deserialize()?; if response != 0 { // 1 is cancelled, 2 is "ended some other way". Neither is an error: // the user is allowed to change their mind. return Ok(Vec::new()); } match results.get("uris") { Some(value) => Ok(Vec::::try_from(value.try_clone()?)?), None => Ok(Vec::new()), } }