// TODO // - Implement StatusNotifierWatcher if one is not running // - Register with StatusNotiferWatcher // - Handle signals for registered/unreigisted items use cascade::cascade; use gtk4::{ gio, glib::{self, clone}, prelude::*, subclass::prelude::*, }; use once_cell::unsync::OnceCell; use std::{borrow::Cow, cell::RefCell, collections::HashMap}; use crate::deref_cell::DerefCell; #[derive(Default)] pub struct StatusAreaInner { box_: DerefCell, watcher: OnceCell, icons: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for StatusAreaInner { const NAME: &'static str = "S76StatusArea"; type ParentType = gtk4::Widget; type Type = StatusArea; fn class_init(klass: &mut Self::Class) { klass.set_layout_manager_type::(); } } impl ObjectImpl for StatusAreaInner { fn constructed(&self, obj: &StatusArea) { let box_ = cascade! { gtk4::Box::new(gtk4::Orientation::Horizontal, 0); ..set_parent(obj); }; self.box_.set(box_); glib::MainContext::default().spawn_local(clone!(@strong obj => async move { let watcher = match StatusNotifierWatcher::new().await { Ok(watcher) => watcher, Err(err) => { eprintln!("Failed to connect to 'org.kde.StatusNotifierWatcher': {}", err); return; } }; for name in watcher.registered_items().await { glib::MainContext::default().spawn_local(clone!(@strong obj => async move { obj.item_registered(&name).await; })); } watcher.connect_item_registered_unregistered(clone!(@strong obj => move |name, registered| { if registered { glib::MainContext::default().spawn_local(clone!(@strong obj => async move { obj.item_registered(&name).await; })); } else { obj.item_unregistered(&name); } })); let _ = obj.inner().watcher.set(watcher); })); } fn dispose(&self, _obj: &StatusArea) { self.box_.unparent(); } } impl WidgetImpl for StatusAreaInner {} glib::wrapper! { pub struct StatusArea(ObjectSubclass) @extends gtk4::Widget; } impl StatusArea { pub fn new() -> Self { glib::Object::new(&[]).unwrap() } fn inner(&self) -> &StatusAreaInner { StatusAreaInner::from_instance(self) } async fn item_registered(&self, name: &str) { match StatusNotifierItem::new(&name).await { Ok(item) => { let image = gtk4::Image::from_icon_name(item.icon_name().as_deref()); self.inner().box_.append(&image); if let Some(menu) = item.menu() { println!("{:?}", menu.get_layout(0, -1, &[]).await); } self.item_unregistered(name); self.inner() .icons .borrow_mut() .insert(name.to_owned(), image); } Err(err) => eprintln!("Failed to connect to '{}': {}", name, err), } } fn item_unregistered(&self, name: &str) { if let Some(icon) = self.inner().icons.borrow_mut().remove(name) { self.inner().box_.remove(&icon); } } } #[derive(Debug)] struct Layout(i32, HashMap, Vec); impl glib::StaticVariantType for Layout { fn static_variant_type() -> Cow<'static, glib::VariantTy> { glib::VariantTy::new("(ia{sv}av)").unwrap().into() } } impl glib::FromVariant for Layout { fn from_variant(variant: &glib::Variant) -> Option { let (id, props, children) = variant.get::<(_, _, Vec)>()?; let children = children.iter().filter_map(Self::from_variant).collect(); Some(Self(id, props, children)) } } #[derive(Clone)] struct DBusMenu(gio::DBusProxy); impl DBusMenu { async fn new(dest: &str, path: &str) -> Result { let proxy = gio::DBusProxy::for_bus_future( gio::BusType::Session, gio::DBusProxyFlags::NONE, None, dest, path, "com.canonical.dbusmenu", ) .await?; Ok(Self(proxy)) } async fn get_layout( &self, parent: i32, depth: i32, properties: &[&str], ) -> Result<(u32, Layout), glib::Error> { // XXX unwrap Ok(self .0 .call_future( "GetLayout", Some(&(parent, depth, properties).to_variant()), gio::DBusCallFlags::NONE, 1000, ) .await? .get() .unwrap()) } } struct StatusNotifierItem(gio::DBusProxy, Option); impl StatusNotifierItem { async fn new(name: &str) -> Result { let idx = name.find('/').unwrap(); let dest = &name[..idx]; let path = &name[idx..]; let proxy = gio::DBusProxy::for_bus_future( gio::BusType::Session, gio::DBusProxyFlags::NONE, None, dest, path, "org.kde.StatusNotifierItem", ) .await?; let menu_path = proxy .cached_property("Menu") .and_then(|x| x.get::()); let menu = if let Some(menu_path) = menu_path { Some(DBusMenu::new(dest, &menu_path).await?) } else { None }; Ok(Self(proxy, menu)) } fn property(&self, prop: &str) -> Option { self.0.cached_property(prop)?.get() } fn icon_name(&self) -> Option { // TODO: IconThemePath? AttentionIconName? self.property("IconName") } fn menu(&self) -> Option { self.1.clone() } } struct StatusNotifierWatcher(gio::DBusProxy); impl StatusNotifierWatcher { async fn new() -> Result { let proxy = gio::DBusProxy::for_bus_future( gio::BusType::Session, gio::DBusProxyFlags::NONE, None, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", "org.kde.StatusNotifierWatcher", ) .await?; Ok(Self(proxy)) } fn property(&self, prop: &str) -> Option { self.0.cached_property(prop)?.get() } async fn registered_items(&self) -> Vec { self.property::>("RegisteredStatusNotifierItems") .unwrap_or_default() } fn connect_item_registered_unregistered( &self, f: F, ) -> glib::SignalHandlerId { self.0 .connect_local("g-signal", false, move |args| { let signal_args = args[3].get::().unwrap(); match args[2].get::().unwrap().as_str() { "StatusNotifierItemRegistered" => { f(signal_args.get::<(String,)>().unwrap().0, true); } "StatusNotifierItemUnregistered" => { f(signal_args.get::<(String,)>().unwrap().0, false); } _ => {} } None }) .unwrap() } }