diff --git a/Cargo.lock b/Cargo.lock index 99c3a702..5071428a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1906,6 +1906,7 @@ name = "notifier_host" version = "0.1.0" dependencies = [ "dbusmenu-gtk3", + "gdk", "gtk", "log", "thiserror", diff --git a/crates/eww/src/widgets/systray.rs b/crates/eww/src/widgets/systray.rs index db2d4ef4..1ed1bad1 100644 --- a/crates/eww/src/widgets/systray.rs +++ b/crates/eww/src/widgets/systray.rs @@ -60,14 +60,14 @@ impl Props { } struct Tray { - menubar: gtk::MenuBar, + container: gtk::Box, items: std::collections::HashMap, icon_size: tokio::sync::watch::Receiver, } -pub fn spawn_systray(menubar: >k::MenuBar, props: &Props) { - let mut systray = Tray { menubar: menubar.clone(), items: Default::default(), icon_size: props.icon_size_tx.subscribe() }; +pub fn spawn_systray(container: >k::Box, props: &Props) { + let mut systray = Tray { container: container.clone(), items: Default::default(), icon_size: props.icon_size_tx.subscribe() }; let task = glib::MainContext::default().spawn_local(async move { let s = match dbus_session().await { @@ -78,14 +78,14 @@ pub fn spawn_systray(menubar: >k::MenuBar, props: &Props) { } }; - systray.menubar.show(); + systray.container.show(); if let Err(e) = notifier_host::run_host_forever(&mut systray, &s.snw).await { log::error!("notifier host error: {}", e); } }); // stop the task when the widget is dropped - menubar.connect_destroy(move |_| { + container.connect_destroy(move |_| { task.abort(); }); } @@ -93,15 +93,15 @@ pub fn spawn_systray(menubar: >k::MenuBar, props: &Props) { impl notifier_host::Host for Tray { fn add_item(&mut self, id: &str, item: notifier_host::Item) { let item = Item::new(id.to_owned(), item, self.icon_size.clone()); - self.menubar.add(&item.widget); + self.container.add(&item.widget); if let Some(old_item) = self.items.insert(id.to_string(), item) { - self.menubar.remove(&old_item.widget); + self.container.remove(&old_item.widget); } } fn remove_item(&mut self, id: &str) { if let Some(item) = self.items.get(id) { - self.menubar.remove(&item.widget); + self.container.remove(&item.widget); } else { log::warn!("Tried to remove nonexistent item {:?} from systray", id); } @@ -111,7 +111,7 @@ impl notifier_host::Host for Tray { /// Item represents a single icon being shown in the system tray. struct Item { /// Main widget representing this tray item. - widget: gtk::MenuItem, + widget: gtk::EventBox, /// Async task to stop when this item gets removed. task: Option>, @@ -127,7 +127,7 @@ impl Drop for Item { impl Item { fn new(id: String, item: notifier_host::Item, icon_size: tokio::sync::watch::Receiver) -> Self { - let widget = gtk::MenuItem::new(); + let widget = gtk::EventBox::new(); let out_widget = widget.clone(); // copy so we can return it let task = glib::MainContext::default().spawn_local(async move { @@ -140,8 +140,8 @@ impl Item { } async fn maintain( - widget: gtk::MenuItem, - item: notifier_host::Item, + widget: gtk::EventBox, + mut item: notifier_host::Item, mut icon_size: tokio::sync::watch::Receiver, ) -> zbus::Result<()> { // init icon @@ -150,9 +150,8 @@ impl Item { icon.show(); // init menu - match item.menu().await { - Ok(m) => widget.set_submenu(Some(&m)), - Err(e) => log::warn!("failed to get menu: {}", e), + if let Err(e) = item.set_menu().await { + log::warn!("failed to get menu: {}", e); } // TODO this is a lot of code duplication unfortunately, i'm not really sure how to @@ -175,7 +174,7 @@ impl Item { let window = widget.toplevel().expect("Failed to obtain toplevel window").downcast::().expect("Failed to downcast window"); widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK); - widget.connect_button_press_event(glib::clone!(@strong item => move |_, evt| { + widget.connect_button_press_event(glib::clone!(@strong item => move |widget, evt| { let (x, y) = (evt.root().0 as i32 + window.x(), evt.root().1 as i32 + window.y()); let item_is_menu = run_async_task(async { item.sni.item_is_menu().await }); let have_item_is_menu = item_is_menu.is_ok(); @@ -189,24 +188,27 @@ impl Item { item_is_menu ); - match (evt.button(), item_is_menu) { + let result = match (evt.button(), item_is_menu) { (gdk::BUTTON_PRIMARY, false) => { - if let Err(e) = run_async_task(async { item.sni.activate(x, y).await }) { - log::error!("failed to send activate event: {}", e); - if !have_item_is_menu { - // Some applications are in fact menu-only (don't have Activate method) - // but don't report so through ItemIsMenu property. Fallback to menu if - // activate failed in this case. - return gtk::Inhibit(false); - } + let result = run_async_task(async { item.sni.activate(x, y).await }); + if result.is_err() && !have_item_is_menu { + log::error!("failed to send activate event: {}", result.unwrap_err()); + // Some applications are in fact menu-only (don't have Activate method) + // but don't report so through ItemIsMenu property. Fallback to menu if + // activate failed in this case. + run_async_task(async { item.popup_menu(widget, evt, x, y).await }) + } else { + result } } - (gdk::BUTTON_MIDDLE, _) => { - if let Err(e) = run_async_task(async { item.sni.secondary_activate(x, y).await }) { - log::error!("failed to send secondary activate event: {}", e); - } + (gdk::BUTTON_MIDDLE, _) => run_async_task(async { item.sni.secondary_activate(x, y).await }), + (gdk::BUTTON_SECONDARY, _) | (gdk::BUTTON_PRIMARY, true) => { + run_async_task(async { item.popup_menu(widget, evt, x, y).await }) } - _ => return gtk::Inhibit(false), + _ => Err(zbus::Error::Failure(format!("unknown button {}", evt.button()))), + }; + if let Err(result) = result { + log::error!("failed to handle mouse click {}: {}", evt.button(), result); } gtk::Inhibit(true) })); diff --git a/crates/eww/src/widgets/widget_definitions.rs b/crates/eww/src/widgets/widget_definitions.rs index e7731c31..a62abd0d 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1047,13 +1047,18 @@ fn build_graph(bargs: &mut BuilderArgs) -> Result { const WIDGET_NAME_SYSTRAY: &str = "systray"; /// @widget systray /// @desc Tray for system notifier icons -fn build_systray(bargs: &mut BuilderArgs) -> Result { - let gtk_widget = gtk::MenuBar::new(); +fn build_systray(bargs: &mut BuilderArgs) -> Result { + let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0); let props = Rc::new(systray::Props::new()); - let props_clone = props.clone(); + let props_clone = props.clone(); // copies for def_widget - // copies for def_widget def_widget!(bargs, _g, gtk_widget, { + // @prop spacing - spacing between elements + prop(spacing: as_i32 = 0) { gtk_widget.set_spacing(spacing) }, + // @prop orientation - orientation of the box. possible values: $orientation + prop(orientation: as_string) { gtk_widget.set_orientation(parse_orientation(&orientation)?) }, + // @prop space-evenly - space the widgets evenly. + prop(space_evenly: as_bool = true) { gtk_widget.set_homogeneous(space_evenly) }, // @prop icon-size - size of icons in the tray prop(icon_size: as_i32) { if icon_size <= 0 { @@ -1062,8 +1067,6 @@ fn build_systray(bargs: &mut BuilderArgs) -> Result { props.icon_size(icon_size); } }, - // @prop pack-direction - how to arrange tray items - prop(pack_direction: as_string) { gtk_widget.set_pack_direction(parse_packdirection(&pack_direction)?); }, }); systray::spawn_systray(>k_widget, &props_clone); @@ -1125,16 +1128,6 @@ fn parse_justification(j: &str) -> Result { } } -/// @var pack-direction - "right", "ltr", "left", "rtl", "down", "ttb", "up", "btt" -fn parse_packdirection(o: &str) -> Result { - enum_parse! { "packdirection", o, - "right" | "ltr" => gtk::PackDirection::Ltr, - "left" | "rtl" => gtk::PackDirection::Rtl, - "down" | "ttb" => gtk::PackDirection::Ttb, - "up" | "btt" => gtk::PackDirection::Btt, - } -} - /// Connect a function to the first map event of a widget. After that first map, the handler will get disconnected. fn connect_first_map, F: Fn(&W) + 'static>(widget: &W, func: F) { let signal_handler_id = std::rc::Rc::new(std::cell::RefCell::new(None)); diff --git a/crates/notifier_host/Cargo.toml b/crates/notifier_host/Cargo.toml index 35c8c079..79db74c3 100644 --- a/crates/notifier_host/Cargo.toml +++ b/crates/notifier_host/Cargo.toml @@ -10,6 +10,7 @@ homepage = "https://github.com/elkowar/eww" [dependencies] gtk = { version = "0.17.1" } +gdk = "0.17.1" log = "0.4" thiserror = "1.0" tokio = { version = "1.31.0", features = ["full"] } diff --git a/crates/notifier_host/src/item.rs b/crates/notifier_host/src/item.rs index 7fc9eb2b..59d91fa1 100644 --- a/crates/notifier_host/src/item.rs +++ b/crates/notifier_host/src/item.rs @@ -61,6 +61,7 @@ fn split_service_name(service: &str) -> zbus::Result<(String, String)> { pub struct Item { pub sni: dbus::StatusNotifierItemProxy<'static>, + gtk_menu: Option, } impl Item { @@ -68,7 +69,7 @@ impl Item { let (addr, path) = split_service_name(addr)?; let sni = dbus::StatusNotifierItemProxy::builder(con).destination(addr)?.path(path)?.build().await?; - Ok(Item { sni }) + Ok(Self { sni, gtk_menu: None }) } /// Get the current status of the item. @@ -80,10 +81,20 @@ impl Item { } } - pub async fn menu(&self) -> zbus::Result { - // TODO better handling if menu() method doesn't exist - let menu = dbusmenu_gtk3::Menu::new(self.sni.destination(), &self.sni.menu().await?); - Ok(menu.upcast()) + pub async fn set_menu(&mut self) -> zbus::Result<()> { + let menu = self.sni.menu().await?; + self.gtk_menu = Some(dbusmenu_gtk3::Menu::new(self.sni.destination(), &menu)); + Ok(()) + } + + pub async fn popup_menu(&self, widget: >k::EventBox, event: &gdk::EventButton, x: i32, y: i32) -> zbus::Result<()> { + if let Some(menu) = &self.gtk_menu { + menu.set_attach_widget(Some(widget)); + menu.popup_at_pointer(event.downcast_ref::()); + Ok(()) + } else { + self.sni.context_menu(x, y).await + } } pub async fn icon(&self, size: i32, scale: i32) -> Option {