diff --git a/src/main.rs b/src/main.rs index 21846960..866f47db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use gtk4::{gdk, glib, prelude::*}; mod deref_cell; mod mpris; mod status_area; +mod status_menu; mod time_button; mod window; mod x; diff --git a/src/status_area.rs b/src/status_area.rs index ac8d2144..70b9fb83 100644 --- a/src/status_area.rs +++ b/src/status_area.rs @@ -3,7 +3,6 @@ // - Register with StatusNotiferWatcher // - Handle signals for registered/unreigisted items -use byte_string::ByteStr; use cascade::cascade; use gtk4::{ gio, @@ -12,15 +11,16 @@ use gtk4::{ subclass::prelude::*, }; use once_cell::unsync::OnceCell; -use std::{borrow::Cow, cell::RefCell, collections::HashMap, fmt}; +use std::{cell::RefCell, collections::HashMap}; use crate::deref_cell::DerefCell; +use crate::status_menu::StatusMenu; #[derive(Default)] pub struct StatusAreaInner { box_: DerefCell, watcher: OnceCell, - icons: RefCell>, + icons: RefCell>, } #[glib::object_subclass] @@ -94,20 +94,15 @@ impl StatusArea { } async fn item_registered(&self, name: &str) { - match StatusNotifierItem::new(&name).await { + match StatusMenu::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.inner().box_.append(&item); self.item_unregistered(name); self.inner() .icons .borrow_mut() - .insert(name.to_owned(), image); + .insert(name.to_owned(), item); } Err(err) => eprintln!("Failed to connect to '{}': {}", name, err), } @@ -120,167 +115,6 @@ impl StatusArea { } } -//#[derive(Debug)] -struct Layout(i32, HashMap, Vec); - -impl fmt::Debug for Layout { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut s = f.debug_struct("Layout"); - s.field("id", &self.0); - for (k, v) in &self.1 { - if let Some(v) = v.get::() { - s.field(k, &v); - } else if let Some(v) = v.get::() { - s.field(k, &v); - } else if let Some(v) = v.get::() { - s.field(k, &v); - } else if let Some(v) = v.get::>() { - s.field(k, &ByteStr::new(&v)); - } else { - s.field(k, v); - } - } - s.field("children", &self.2); - s.finish() - } -} - -#[allow(dead_code)] -impl Layout { - fn prop(&self, name: &str) -> Option { - self.1.get(name)?.get() - } - - fn accessible_desc(&self) -> Option { - self.prop("accessible-desc") - } - - fn children_display(&self) -> Option { - self.prop("children-display") - } - - fn label(&self) -> Option { - self.prop("label") - } - - fn enabled(&self) -> Option { - self.prop("enabled") - } - - fn visible(&self) -> Option { - self.prop("visible") - } - - fn type_(&self) -> Option { - self.prop("type") - } - - fn toggle_type(&self) -> Option { - self.prop("toggle-type") - } - - fn toggle_state(&self) -> Option { - self.prop("toggle-state") - } - - fn icon_data(&self) -> Option> { - self.prop("icon-data") - } -} - -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 { diff --git a/src/status_menu.rs b/src/status_menu.rs new file mode 100644 index 00000000..40c70976 --- /dev/null +++ b/src/status_menu.rs @@ -0,0 +1,224 @@ +use byte_string::ByteStr; +use cascade::cascade; +use gtk4::{gio, glib, prelude::*, subclass::prelude::*}; +use std::{borrow::Cow, collections::HashMap, fmt}; + +use crate::deref_cell::DerefCell; + +#[derive(Default)] +pub struct StatusMenuInner { + menu_button: DerefCell, +} + +#[glib::object_subclass] +impl ObjectSubclass for StatusMenuInner { + const NAME: &'static str = "S76StatusMenu"; + type ParentType = gtk4::Widget; + type Type = StatusMenu; + + fn class_init(klass: &mut Self::Class) { + klass.set_layout_manager_type::(); + } +} + +impl ObjectImpl for StatusMenuInner { + fn constructed(&self, obj: &StatusMenu) { + let menu_button = cascade! { + gtk4::MenuButton::new(); + ..set_parent(obj); + }; + + self.menu_button.set(menu_button); + } + + fn dispose(&self, _obj: &StatusMenu) { + self.menu_button.unparent(); + } +} + +impl WidgetImpl for StatusMenuInner {} + +glib::wrapper! { + pub struct StatusMenu(ObjectSubclass) + @extends gtk4::Widget; +} + +impl StatusMenu { + pub async fn new(name: &str) -> Result { + let item = StatusNotifierItem::new(name).await?; + let obj = glib::Object::new::(&[]).unwrap(); + if let Some(icon_name) = item.icon_name().as_deref() { + obj.inner().menu_button.set_icon_name(&icon_name); + } + + if let Some(menu) = item.menu() { + println!("{:#?}", menu.get_layout(0, -1, &[]).await); + } + + Ok(obj) + } + + fn inner(&self) -> &StatusMenuInner { + StatusMenuInner::from_instance(self) + } +} + +struct Layout(i32, HashMap, Vec); + +impl fmt::Debug for Layout { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut s = f.debug_struct("Layout"); + s.field("id", &self.0); + for (k, v) in &self.1 { + if let Some(v) = v.get::() { + s.field(k, &v); + } else if let Some(v) = v.get::() { + s.field(k, &v); + } else if let Some(v) = v.get::() { + s.field(k, &v); + } else if let Some(v) = v.get::>() { + s.field(k, &ByteStr::new(&v)); + } else { + s.field(k, v); + } + } + s.field("children", &self.2); + s.finish() + } +} + +#[allow(dead_code)] +impl Layout { + fn prop(&self, name: &str) -> Option { + self.1.get(name)?.get() + } + + fn accessible_desc(&self) -> Option { + self.prop("accessible-desc") + } + + fn children_display(&self) -> Option { + self.prop("children-display") + } + + fn label(&self) -> Option { + self.prop("label") + } + + fn enabled(&self) -> Option { + self.prop("enabled") + } + + fn visible(&self) -> Option { + self.prop("visible") + } + + fn type_(&self) -> Option { + self.prop("type") + } + + fn toggle_type(&self) -> Option { + self.prop("toggle-type") + } + + fn toggle_state(&self) -> Option { + self.prop("toggle-state") + } + + fn icon_data(&self) -> Option> { + self.prop("icon-data") + } +} + +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() + } +}