From 6447075a7758c5b05daa6d93164c617fc25dc875 Mon Sep 17 00:00:00 2001 From: Maximiliano Sandoval R Date: Sun, 5 Jun 2022 00:02:26 +0200 Subject: [PATCH] Add activation_token Fixes: https://github.com/bilelmoussaoui/ashpd/issues/55 --- Cargo.toml | 4 +- src/activation_token/gtk3.rs | 17 ++++ src/activation_token/gtk4.rs | 40 ++++++++ src/activation_token/mod.rs | 119 ++++++++++++++++++++++ src/activation_token/wayland.rs | 175 ++++++++++++++++++++++++++++++++ src/desktop/dynamic_launcher.rs | 18 +++- src/desktop/open_uri.rs | 20 +++- src/lib.rs | 2 + 8 files changed, 386 insertions(+), 9 deletions(-) create mode 100644 src/activation_token/gtk3.rs create mode 100644 src/activation_token/gtk4.rs create mode 100644 src/activation_token/mod.rs create mode 100644 src/activation_token/wayland.rs diff --git a/Cargo.toml b/Cargo.toml index a1b7e0f21..96f8e927f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ gdk3x11 = {package = "gdkx11", version = "0.16", optional = true} gdk3wayland = {package = "gdkwayland", version = "0.16", optional = true} gtk3 = {package = "gtk", version = "0.16", optional = true} -gdk4wayland = {package = "gdk4-wayland", version = "0.5", optional = true} +gdk4wayland = {package = "gdk4-wayland", version = "0.5", optional = true, features = ["wayland_crate"]} gdk4x11 = {package = "gdk4-x11", version = "0.5", optional = true} gtk4 = {version = "0.5", optional = true} @@ -44,7 +44,7 @@ tracing = {version = "0.1", optional = true} libc = {version = "0.2", optional = true} raw-window-handle = {version = "0.5", optional = true} wayland-client = {version = "0.30.0-beta.10", optional = true} -wayland-protocols = {version = "0.30.0-beta.10", optional = true, features = ["unstable", "client"]} +wayland-protocols = {version = "0.30.0-beta.10", optional = true, features = ["unstable", "client", "staging"]} wayland-backend = {version = "0.1.0-beta.10", optional = true, features = ["client_system"]} async-std = {version = "1.12", optional = true} tokio = {version = "1.21", features = ["fs", "io-util"], optional = true, default-features = false} diff --git a/src/activation_token/gtk3.rs b/src/activation_token/gtk3.rs new file mode 100644 index 000000000..49d5a878c --- /dev/null +++ b/src/activation_token/gtk3.rs @@ -0,0 +1,17 @@ +use gtk3::prelude::*; +use gtk3::{gdk, glib}; + +#[derive(Debug)] +pub struct Gtk3ActivationToken { + pub(crate) token: String, +} + +impl Gtk3ActivationToken { + pub fn from_window(window: &impl glib::IsA) -> Option { + let display = window.as_ref().display(); + match display.backend() { + gdk::Backend::Wayland => todo!(), + _ => None, + } + } +} diff --git a/src/activation_token/gtk4.rs b/src/activation_token/gtk4.rs new file mode 100644 index 000000000..1ace0d163 --- /dev/null +++ b/src/activation_token/gtk4.rs @@ -0,0 +1,40 @@ +#[cfg(feature = "wayland")] +use super::wayland::WaylandActivationToken; +use gdk4wayland::prelude::WaylandSurfaceExtManual; +use gtk4::{gdk, glib, prelude::*}; + +#[derive(Debug)] +pub struct Gtk4ActivationToken { + pub(crate) wl_token: WaylandActivationToken, +} + +#[cfg(all(feature = "gtk4_wayland", feature = "wayland"))] +impl Gtk4ActivationToken { + pub async fn from_native>(app_id: &str, native: &N) -> Option { + let surface = native.surface(); + match surface.display().backend() { + gdk::Backend::Wayland => { + let surface = surface + .downcast_ref::() + .unwrap(); + if let Some(wl_surface) = surface.wl_surface() { + let wl_token = WaylandActivationToken::from_surface(app_id, &wl_surface) + .await + .unwrap(); + + Some(Self { wl_token }) + } else { + None + } + } + _ => None, + } + } +} + +#[cfg(feature = "wayland")] +impl From for Gtk4ActivationToken { + fn from(wl_token: WaylandActivationToken) -> Self { + Self { wl_token } + } +} diff --git a/src/activation_token/mod.rs b/src/activation_token/mod.rs new file mode 100644 index 000000000..e0d2f7af3 --- /dev/null +++ b/src/activation_token/mod.rs @@ -0,0 +1,119 @@ +#[cfg(all(feature = "gtk3", feature = "wayland"))] +mod gtk3; +#[cfg(all(feature = "gtk3", feature = "wayland"))] +pub use self::gtk3::Gtk3ActivationToken; + +#[cfg(feature = "gtk4_wayland")] +mod gtk4; +#[cfg(feature = "gtk4_wayland")] +pub use self::gtk4::Gtk4ActivationToken; + +#[cfg(any(feature = "wayland"))] +mod wayland; +#[cfg(feature = "wayland")] +pub use wayland::WaylandActivationToken; + +use serde::{ser::Serializer, Serialize}; +use zbus::zvariant::Type; + +// TODO +/// See https://wayland.app/protocols/xdg-activation-v1 +#[derive(Type)] +#[zvariant(signature = "s")] +#[derive(Debug)] +pub enum ActivationToken { + #[cfg(feature = "wayland")] + #[doc(hidden)] + Wayland(WaylandActivationToken), + #[cfg(feature = "gtk4_wayland")] + #[doc(hidden)] + Gtk4(Gtk4ActivationToken), + #[cfg(all(feature = "gtk3", feature = "wayland"))] + #[doc(hidden)] + Gtk3(Gtk3ActivationToken), + #[doc(hidden)] + Raw(String), + #[doc(hidden)] + None, +} + +impl Serialize for ActivationToken { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl Default for ActivationToken { + fn default() -> Self { + Self::None + } +} + +impl ActivationToken { + #[cfg(feature = "wayland")] + /// Create an instance of [`ActivationToken`] from a Wayland surface and the + /// application's id. + pub async fn from_wayland_surface( + app_id: &str, + surface: &wayland_client::protocol::wl_surface::WlSurface, + ) -> Option { + let token = WaylandActivationToken::from_surface(app_id, surface).await?; + + Some(Self::Wayland(token)) + } + + #[cfg(feature = "wayland")] + /// Create an instance of [`ActivationToken`] from a raw Wayland surface and + /// the application's id. + /// + /// # Safety + /// + /// Both pointers have to be valid surface and display pointers. You must + /// ensure the `display_ptr` lives longer than the returned + /// `ActivationToken`. + pub async unsafe fn from_wayland_raw( + app_id: &str, + surface_ptr: *mut std::ffi::c_void, + display_ptr: *mut std::ffi::c_void, + ) -> Option { + let token = WaylandActivationToken::from_raw(app_id, surface_ptr, display_ptr).await?; + + Some(Self::Wayland(token)) + } + + #[cfg(feature = "gtk4_wayland")] + // TODO Maybe name from_display. + /// Creates a [`ActivationToken`] from a [`gtk4::Native`](https://docs.gtk.org/gtk4/class.Native.html). + pub async fn from_native>( + app_id: &str, + native: &N, + ) -> Option { + let token = Gtk4ActivationToken::from_native(app_id, native).await?; + + Some(Self::Gtk4(token)) + } + + #[cfg(all(feature = "gtk3", feature = "wayland"))] + /// Creates a [`ActivationToken`] from a [`IsA`](https://gtk-rs.org/gtk3-rs/stable/latest/docs/gdk/struct.Window.html). + pub fn from_window(window: &impl ::gtk3::glib::IsA<::gtk3::gdk::Window>) -> Option { + let token = Gtk3ActivationToken::from_window(window)?; + + Some(Self::Gtk3(token)) + } + + pub(crate) fn as_str(&self) -> &str { + match self { + #[cfg(feature = "wayland")] + Self::Wayland(activation_token) => activation_token.token.as_str(), + #[cfg(feature = "gtk4_wayland")] + Self::Gtk4(activation_token) => activation_token.wl_token.token.as_str(), + #[cfg(all(feature = "gtk3", feature = "wayland"))] + Self::Gtk3(activation_token) => activation_token.token.as_str(), + Self::Raw(string) => string.as_str(), + Self::None => "", + } + } +} diff --git a/src/activation_token/wayland.rs b/src/activation_token/wayland.rs new file mode 100644 index 000000000..fdb9e8422 --- /dev/null +++ b/src/activation_token/wayland.rs @@ -0,0 +1,175 @@ +use wayland_backend::sys::client::Backend; +use wayland_client::{ + protocol::{wl_registry, wl_surface::WlSurface}, + Proxy, QueueHandle, +}; +use wayland_protocols::xdg::activation::v1::client::{ + xdg_activation_token_v1::{Event, XdgActivationTokenV1}, + xdg_activation_v1::XdgActivationV1, +}; + +// Supported versions. +const XDG_ACTIVATION_V1_VERSION: u32 = 1; + +#[derive(Debug, Default)] +pub struct WaylandActivationToken { + pub(crate) token: String, + wl_activation: Option, + wl_token: Option, +} + +impl Drop for WaylandActivationToken { + fn drop(&mut self) { + if let Some(wl_token) = self.wl_token.take() { + wl_token.destroy(); + } + + if let Some(wl_activation) = self.wl_activation.take() { + wl_activation.destroy(); + } + } +} + +impl WaylandActivationToken { + // Can be changed to display. + pub async fn from_surface(app_id: &str, surface: &WlSurface) -> Option { + let backend = surface.backend().upgrade()?; + let conn = wayland_client::Connection::from_backend(backend); + + Self::new_inner(app_id, conn, surface).await + } + + pub async unsafe fn from_raw( + app_id: &str, + surface_ptr: *mut std::ffi::c_void, + display_ptr: *mut std::ffi::c_void, + ) -> Option { + if surface_ptr.is_null() || display_ptr.is_null() { + return None; + } + + let backend = Backend::from_foreign_display(display_ptr as *mut _); + let conn = wayland_client::Connection::from_backend(backend); + let obj_id = wayland_backend::sys::client::ObjectId::from_ptr( + WlSurface::interface(), + surface_ptr as *mut _, + ) + .ok()?; + + let surface = WlSurface::from_id(&conn, obj_id).ok()?; + + Self::new_inner(app_id, conn, &surface).await + } + + async fn new_inner( + app_id: &str, + conn: wayland_client::Connection, + surface: &WlSurface, + ) -> Option { + let (sender, receiver) = futures_channel::oneshot::channel::>(); + + // Cheap clone, protocol objects are essentially smart pointers + let surface = surface.clone(); + let app_id = app_id.to_owned(); + std::thread::spawn(move || match wayland_export_token(app_id, conn, &surface) { + Ok(window_handle) => sender.send(Some(window_handle)).unwrap(), + Err(_err) => { + #[cfg(feature = "tracing")] + tracing::info!("Could not get wayland window identifier: {_err}"); + sender.send(None).unwrap(); + } + }); + + receiver.await.unwrap() + } +} + +impl wayland_client::Dispatch for WaylandActivationToken { + fn event( + state: &mut Self, + _proxy: &XdgActivationTokenV1, + event: ::Event, + _data: &(), + _connhandle: &wayland_client::Connection, + _qhandle: &QueueHandle, + ) { + if let Event::Done { token } = event { + state.token = token; + } + } +} + +impl wayland_client::Dispatch for WaylandActivationToken { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _data: &(), + _connhandle: &wayland_client::Connection, + qhandle: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, + interface, + version, + } = event + { + if &interface == "xdg_activation_v1" { + #[cfg(feature = "tracing")] + tracing::info!("Found wayland interface {interface} v{version}"); + let activation = registry.bind::( + name, + version.min(XDG_ACTIVATION_V1_VERSION), + qhandle, + (), + ); + state.wl_activation = Some(activation); + } + } + } +} + +impl wayland_client::Dispatch for WaylandActivationToken { + fn event( + _state: &mut Self, + _activation: &XdgActivationV1, + _event: wayland_protocols::xdg::activation::v1::client::xdg_activation_v1::Event, + _data: &(), + _connhandle: &wayland_client::Connection, + _qhandle: &QueueHandle, + ) { + } +} + +fn wayland_export_token( + app_id: String, + conn: wayland_client::Connection, + surface: &WlSurface, +) -> Result> { + let display = conn.display(); + let mut event_queue = conn.new_event_queue(); + let mut state = WaylandActivationToken::default(); + let qhandle = event_queue.handle(); + display.get_registry(&qhandle, ()); + event_queue.roundtrip(&mut state)?; + + if let Some(ref activation) = state.wl_activation { + let wl_token = activation.get_activation_token(&qhandle, ()); + // TODO is this an APP ID in the traditional sense? + wl_token.set_app_id(app_id); + wl_token.set_surface(surface); + // TODO wl_token.set_serial(serial, &seat); + wl_token.commit(); + + event_queue.roundtrip(&mut state)?; + state.wl_token = Some(wl_token); + }; + + if !state.token.is_empty() { + Ok(state) + } else { + #[cfg(feature = "tracing")] + tracing::error!("Failed to get a response from the wayland server"); + Err(Box::new(crate::Error::NoResponse)) + } +} diff --git a/src/desktop/dynamic_launcher.rs b/src/desktop/dynamic_launcher.rs index c73ac38ea..31ac8e73b 100644 --- a/src/desktop/dynamic_launcher.rs +++ b/src/desktop/dynamic_launcher.rs @@ -46,7 +46,7 @@ use zbus::zvariant::{self, SerializeDict, Type}; use super::{HandleToken, Icon, DESTINATION, PATH}; use crate::{ helpers::{call_method, call_request_method, session_connection}, - Error, WindowIdentifier, + ActivationToken, Error, WindowIdentifier, }; #[bitflags] @@ -248,9 +248,19 @@ impl<'a> DynamicLauncherProxy<'a> { /// See also [`Launch`](https://flatpak.github.io/xdg-desktop-portal/index.html#gdbus-method-org-freedesktop-portal-DynamicLauncher.Launch). #[doc(alias = "Launch")] #[doc(alias = "xdp_portal_dynamic_launcher_launch")] - pub async fn launch(&self, desktop_file_id: &str) -> Result<(), Error> { - // TODO: handle activation_token - let options: HashMap<&str, zvariant::Value<'_>> = HashMap::new(); + pub async fn launch( + &self, + desktop_file_id: &str, + activation_token: ActivationToken, + ) -> Result<(), Error> { + let mut options: HashMap<&str, zvariant::Value<'_>> = HashMap::new(); + match activation_token { + ActivationToken::None => (), + token => { + let token = token.as_str().to_owned(); + options.insert("activation_token", token.into()); + } + }; call_method(self.inner(), "Launch", &(desktop_file_id, &options)).await } diff --git a/src/desktop/open_uri.rs b/src/desktop/open_uri.rs index e5c6281aa..47a9b3271 100644 --- a/src/desktop/open_uri.rs +++ b/src/desktop/open_uri.rs @@ -57,14 +57,14 @@ use zbus::zvariant::{Fd, SerializeDict, Type}; use super::{HandleToken, DESTINATION, PATH}; use crate::{ helpers::{call_basic_response_method, session_connection}, - Error, WindowIdentifier, + ActivationToken, Error, WindowIdentifier, }; #[derive(SerializeDict, Type, Debug, Default)] #[zvariant(signature = "dict")] struct OpenDirOptions { handle_token: HandleToken, - activation_token: Option, + activation_token: ActivationToken, } #[derive(SerializeDict, Type, Debug, Default)] @@ -73,7 +73,7 @@ struct OpenFileOptions { handle_token: HandleToken, writeable: Option, ask: Option, - activation_token: Option, + activation_token: ActivationToken, } #[derive(Debug)] @@ -171,6 +171,13 @@ impl OpenFileRequest { self } + #[must_use] + /// Sets the activation token for the application. + pub fn activation_token(mut self, activation_token: ActivationToken) -> Self { + self.options.activation_token = activation_token; + self + } + pub async fn build_file(self, file: &impl AsRawFd) -> Result<(), Error> { let proxy = OpenURIProxy::new().await?; proxy.open_file(&self.identifier, file, self.options).await @@ -198,6 +205,13 @@ impl OpenDirectoryRequest { self } + #[must_use] + /// Sets the activation token for the application. + pub fn activation_token(mut self, activation_token: ActivationToken) -> Self { + self.options.activation_token = activation_token; + self + } + pub async fn build(self, directory: &impl AsRawFd) -> Result<(), Error> { let proxy = OpenURIProxy::new().await?; proxy diff --git a/src/lib.rs b/src/lib.rs index 27940c8de..a1135eff7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,6 +17,8 @@ static SESSION: OnceCell = OnceCell::new(); /// Alias for a [`Result`] with the error type `ashpd::Error`. pub type Result = std::result::Result; +mod activation_token; +pub use activation_token::ActivationToken; /// Interact with the user's desktop such as taking a screenshot, setting a /// background or querying the user's location. pub mod desktop;