diff --git a/ashpd-demo/Cargo.lock b/ashpd-demo/Cargo.lock index b5b00e01c..f6738ff9c 100644 --- a/ashpd-demo/Cargo.lock +++ b/ashpd-demo/Cargo.lock @@ -44,9 +44,8 @@ checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "ashpd" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" +version = "0.9.0" +source = "git+https://github.com/dcz-self/ashpd/?branch=reb#59bc64dd7260ca64592ba452ac4108570b9b5315" dependencies = [ "async-fs", "async-net", diff --git a/ashpd-demo/Cargo.toml b/ashpd-demo/Cargo.toml index 4086dee25..210fb2415 100644 --- a/ashpd-demo/Cargo.toml +++ b/ashpd-demo/Cargo.toml @@ -7,7 +7,7 @@ version = "0.4.1" [dependencies] adw = {version = "0.6", package = "libadwaita", features = ["v1_4"]} anyhow = "1.0" -ashpd = {version = "^0.8", features = ["gtk4", "tracing", "pipewire"]} +ashpd = {git = "https://github.com/dcz-self/ashpd/", branch = "reb", features = ["gtk4", "tracing", "pipewire"]} chrono = {version = "0.4", default-features = false, features = ["clock"]} futures-util = "0.3" gettext-rs = {version = "0.7", features = ["gettext-system"]} diff --git a/ashpd-demo/data/resources.gresource.xml b/ashpd-demo/data/resources.gresource.xml index 1202c6af2..a8a49333a 100644 --- a/ashpd-demo/data/resources.gresource.xml +++ b/ashpd-demo/data/resources.gresource.xml @@ -16,6 +16,7 @@ resources/ui/email.ui resources/ui/file_chooser.ui resources/ui/inhibit.ui + resources/ui/global_shortcuts.ui resources/ui/location.ui resources/ui/network_monitor.ui resources/ui/notification.ui diff --git a/ashpd-demo/data/resources/ui/global_shortcuts.ui b/ashpd-demo/data/resources/ui/global_shortcuts.ui new file mode 100644 index 000000000..a80dfcb36 --- /dev/null +++ b/ashpd-demo/data/resources/ui/global_shortcuts.ui @@ -0,0 +1,93 @@ + + + + diff --git a/ashpd-demo/data/resources/ui/window.ui b/ashpd-demo/data/resources/ui/window.ui index a95869152..781040c9f 100644 --- a/ashpd-demo/data/resources/ui/window.ui +++ b/ashpd-demo/data/resources/ui/window.ui @@ -101,6 +101,12 @@ file_chooser + + + Global Shortcuts + global_shortcuts + + Inhibit @@ -261,6 +267,14 @@ + + + global_shortcuts + + + + + location diff --git a/ashpd-demo/po/POTFILES.in b/ashpd-demo/po/POTFILES.in index 64d5fd9b6..451fcc1f4 100644 --- a/ashpd-demo/po/POTFILES.in +++ b/ashpd-demo/po/POTFILES.in @@ -7,6 +7,7 @@ data/resources/ui/camera.ui data/resources/ui/email.ui data/resources/ui/file_chooser.ui data/resources/ui/inhibit.ui +data/resources/ui/global_shortcuts.ui data/resources/ui/location.ui data/resources/ui/notification.ui data/resources/ui/open_uri.ui diff --git a/ashpd-demo/src/portals/desktop/global_shortcuts.rs b/ashpd-demo/src/portals/desktop/global_shortcuts.rs new file mode 100644 index 000000000..e439bf7d0 --- /dev/null +++ b/ashpd-demo/src/portals/desktop/global_shortcuts.rs @@ -0,0 +1,277 @@ +use std::{collections::HashSet, sync::Arc}; + +use adw::subclass::prelude::*; +use ashpd::{ + desktop::{ + global_shortcuts::{Activated, Deactivated, ShortcutsChanged, GlobalShortcuts, NewShortcut, Shortcut}, + ResponseError, + Session, + }, + WindowIdentifier, +}; +use gtk::{glib, prelude::*}; +use futures_util::{ + future::{AbortHandle, Abortable}, + lock::Mutex, + stream::{select_all, Stream, StreamExt}, +}; +use crate::widgets::{PortalPage, PortalPageExt, PortalPageImpl}; + +#[derive(Debug)] +enum Event { + Activated(Activated), + Deactivated(Deactivated), + ShortcutsChanged(ShortcutsChanged), +} + +#[derive(Debug, Clone)] +pub struct RegisteredShortcut { + id: String, + activation: String, +} + +mod imp { + use super::*; + + #[derive(Debug, gtk::CompositeTemplate, Default)] + #[template(resource = "/com/belmoussaoui/ashpd/demo/global_shortcuts.ui")] + pub struct GlobalShortcutsPage { + #[template_child] + pub shortcuts: TemplateChild, + #[template_child] + pub activations_group: TemplateChild, + #[template_child] + pub activations_label: TemplateChild, + #[template_child] + pub rebind_count_label: TemplateChild, + pub rebind_count: Arc>, + pub session: Arc>>>, + pub abort_handle: Arc>>, + pub triggers: Arc>>, + pub activations: Arc>>, + } + + #[glib::object_subclass] + impl ObjectSubclass for GlobalShortcutsPage { + const NAME: &'static str = "GlobalShortcutsPage"; + type Type = super::GlobalShortcutsPage; + type ParentType = PortalPage; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + + klass.install_action_async("global_shortcuts.start_session", None, |page, _, _| async move { + if let Err(err) = page.start_session().await { + tracing::error!("Failed to request {}", err); + } + }); + klass.install_action_async("global_shortcuts.stop", None, |page, _, _| async move { + page.stop().await; + }); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + impl ObjectImpl for GlobalShortcutsPage { + fn constructed(&self) { + self.parent_constructed(); + self.obj().action_set_enabled("global_shortcuts.stop", false); + } + } + impl WidgetImpl for GlobalShortcutsPage {} + impl BinImpl for GlobalShortcutsPage {} + impl PortalPageImpl for GlobalShortcutsPage {} +} + +glib::wrapper! { + pub struct GlobalShortcutsPage(ObjectSubclass) + @extends gtk::Widget, adw::Bin, PortalPage; +} + +impl GlobalShortcutsPage { + async fn start_session(&self) -> ashpd::Result<()> { + let root = self.native().unwrap(); + let imp = self.imp(); + let identifier = WindowIdentifier::from_native(&root).await; + let shortcuts = imp.shortcuts.text(); + let shortcuts: Option> = shortcuts.as_str().split(',') + .map(|desc| { + let mut split = desc.splitn(3, ':'); + let name = split.next()?; + let desc = split.next()?; + let trigger = split.next(); + Some(NewShortcut::new(name, desc).preferred_trigger(trigger)) + }).collect(); + + match shortcuts { + Some(shortcuts) => { + let global_shortcuts = GlobalShortcuts::new().await?; + let session = global_shortcuts.create_session().await?; + let request = global_shortcuts.bind_shortcuts(&session, &shortcuts[..], &identifier).await?; + let response = request.response(); + if let Err(e) = &response { + self.error(&match e { + ashpd::Error::Response(ResponseError::Cancelled) => "Cancelled".into(), + ashpd::Error::Response(ResponseError::Other) => "Other response error".into(), + other => format!("{}", other), + }) + }; + imp.activations_group.set_visible(response.is_ok()); + self.action_set_enabled("global_shortcuts.stop", response.is_ok()); + self.action_set_enabled("global_shortcuts.start_session", !response.is_ok()); + self.imp().shortcuts.set_editable(!response.is_ok()); + match response { + Ok(resp) => { + let triggers: Vec<_> + = resp.shortcuts().iter() + .map(|s: &Shortcut| RegisteredShortcut { + id: s.id().to_owned(), + activation: s.trigger_description().to_owned(), + }) + .collect(); + *imp.triggers.lock().await = triggers; + self.display_activations().await; + self.set_rebind_count(Some(0)); + imp.session.lock().await.replace(session); + loop { + if imp.session.lock().await.is_none() { + break; + } + + let (abort_handle, abort_registration) = AbortHandle::new_pair(); + let future = Abortable::new( + self.track_incoming_events(&global_shortcuts), + abort_registration, + ); + imp.abort_handle.lock().await.replace(abort_handle); + let _ = future.await; + } + }, + Err(e) => { + tracing::warn!("Failure {:?}", e); + } + } + }, + _ => { + self.error("Shortcut list invalid"); + } + }; + + Ok(()) + } + + fn set_rebind_count(&self, count: Option) { + let label = &self.imp().rebind_count_label; + match count { + None => label.set_text(""), + Some(count) => label.set_text(&format!("{}", count)), + } + } + + async fn track_incoming_events(&self, global_shortcuts: &GlobalShortcuts<'_>) { + let Ok(activated_stream) = global_shortcuts.receive_activated().await + else { + return; + }; + let Ok(deactivated_stream) = global_shortcuts.receive_deactivated().await + else { + return; + }; + let Ok(changed_stream) = global_shortcuts.receive_shortcuts_changed().await + else { + return; + }; + + let bact: Box + Unpin> = Box::new(activated_stream.map(Event::Activated)); + let bdeact: Box + Unpin> = Box::new(deactivated_stream.map(Event::Deactivated)); + let bchg: Box + Unpin> = Box::new(changed_stream.map(Event::ShortcutsChanged)); + + let mut events = select_all([ + bact, bdeact, bchg, + ]); + + while let Some(event) = events.next().await { + match event { + Event::Activated(activation) => { + self.on_activated(activation).await; + }, + Event::Deactivated(deactivation) => { + self.on_deactivated(deactivation).await; + }, + Event::ShortcutsChanged(change) => { + self.on_changed(change).await; + }, + } + } + } + + async fn stop(&self) { + let imp = self.imp(); + self.action_set_enabled("global_shortcuts.stop", false); + self.action_set_enabled("global_shortcuts.start_session", true); + self.imp().shortcuts.set_editable(true); + + if let Some(abort_handle) = self.imp().abort_handle.lock().await.take() { + abort_handle.abort(); + } + + if let Some(session) = imp.session.lock().await.take() { + let _ = session.close().await; + } + imp.activations_group.set_visible(false); + self.set_rebind_count(None); + imp.activations.lock().await.clear(); + imp.triggers.lock().await.clear(); + } + + async fn display_activations(&self) { + let activations = self.imp().activations.lock().await.clone(); + let triggers = self.imp().triggers.lock().await.clone(); + let text: Vec = triggers.into_iter() + .map(|RegisteredShortcut { id, activation }| { + let escape = |s: &str| glib::markup_escape_text(s).to_string(); + let id = escape(&id); + let activation = escape(&activation); + if activations.contains(&id) { + format!("{}: {}", id, activation) + } else { + format!("{}: {}", id, activation) + } + }) + .collect(); + self.imp().activations_label.set_markup(&text.join("\n")) + } + + async fn on_activated(&self, activation: Activated) { + { + let mut activations = self.imp().activations.lock().await; + activations.insert(activation.shortcut_id().into()); + } + self.display_activations().await + } + + async fn on_deactivated(&self, deactivation: Deactivated) { + { + let mut activations = self.imp().activations.lock().await; + if !activations.remove(deactivation.shortcut_id()) { + tracing::warn!("Received deactivation without previous activation: {:?}", deactivation); + } + } + self.display_activations().await + } + + async fn on_changed(&self, change: ShortcutsChanged) { + *self.imp().triggers.lock().await + = change.shortcuts().iter() + .map(|s| RegisteredShortcut{ + id: s.id().to_owned(), + activation: s.trigger_description().to_owned(), + }) + .collect(); + *self.imp().rebind_count.lock().await += 1; + self.set_rebind_count(Some(*self.imp().rebind_count.lock().await)); + self.display_activations().await + } +} diff --git a/ashpd-demo/src/portals/desktop/mod.rs b/ashpd-demo/src/portals/desktop/mod.rs index 79a12f47d..8c174d4cf 100644 --- a/ashpd-demo/src/portals/desktop/mod.rs +++ b/ashpd-demo/src/portals/desktop/mod.rs @@ -5,6 +5,7 @@ mod device; mod dynamic_launcher; mod email; mod file_chooser; +mod global_shortcuts; mod inhibit; mod location; mod network_monitor; @@ -25,6 +26,7 @@ pub use device::DevicePage; pub use dynamic_launcher::DynamicLauncherPage; pub use email::EmailPage; pub use file_chooser::FileChooserPage; +pub use global_shortcuts::GlobalShortcutsPage; pub use inhibit::InhibitPage; pub use location::LocationPage; pub use network_monitor::NetworkMonitorPage; diff --git a/ashpd-demo/src/window.rs b/ashpd-demo/src/window.rs index 370307fff..2c61e8ef5 100644 --- a/ashpd-demo/src/window.rs +++ b/ashpd-demo/src/window.rs @@ -12,7 +12,7 @@ use crate::{ portals::{ desktop::{ AccountPage, BackgroundPage, CameraPage, DevicePage, DynamicLauncherPage, EmailPage, - FileChooserPage, InhibitPage, LocationPage, NetworkMonitorPage, NotificationPage, + FileChooserPage, InhibitPage, GlobalShortcutsPage, LocationPage, NetworkMonitorPage, NotificationPage, OpenUriPage, PrintPage, ProxyResolverPage, RemoteDesktopPage, ScreenCastPage, ScreenshotPage, SecretPage, WallpaperPage, }, @@ -63,6 +63,8 @@ mod imp { #[template_child] pub inhibit: TemplateChild, #[template_child] + pub global_shortcuts: TemplateChild, + #[template_child] pub secret: TemplateChild, #[template_child] pub remote_desktop: TemplateChild, @@ -98,6 +100,7 @@ mod imp { file_chooser: TemplateChild::default(), open_uri: TemplateChild::default(), inhibit: TemplateChild::default(), + global_shortcuts: TemplateChild::default(), secret: TemplateChild::default(), remote_desktop: TemplateChild::default(), print: TemplateChild::default(),