diff --git a/Cargo.lock b/Cargo.lock index 0ab6a187..8e3af352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1954,6 +1954,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 13e381ea..2a9b46f1 100644 --- a/crates/eww/src/widgets/systray.rs +++ b/crates/eww/src/widgets/systray.rs @@ -53,14 +53,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 { @@ -71,13 +71,13 @@ pub fn spawn_systray(menubar: >k::MenuBar, props: &Props) { } }; - systray.menubar.show(); + systray.container.show(); let e = notifier_host::run_host(&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(); }); } @@ -85,15 +85,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); } @@ -103,7 +103,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>, @@ -119,7 +119,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 { @@ -132,8 +132,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 @@ -142,9 +142,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(&widget).await { + log::warn!("failed to set menu: {}", e); } // TODO this is a lot of code duplication unfortunately, i'm not really sure how to @@ -181,24 +180,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::debug!("fallback to context menu due to: {}", 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( 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( 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 015344c4..18dff789 100644 --- a/crates/eww/src/widgets/widget_definitions.rs +++ b/crates/eww/src/widgets/widget_definitions.rs @@ -1138,13 +1138,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 { @@ -1153,8 +1158,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); @@ -1239,16 +1242,6 @@ fn parse_gravity(g: &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 a03a2a2f..10a7d6cb 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 = "0.17.1" +gdk = "0.17.1" zbus = { version = "3.7.0", default-features = false, features = ["tokio"] } dbusmenu-gtk3 = "0.1.0" diff --git a/crates/notifier_host/src/item.rs b/crates/notifier_host/src/item.rs index c258957e..70317ebd 100644 --- a/crates/notifier_host/src/item.rs +++ b/crates/notifier_host/src/item.rs @@ -42,6 +42,7 @@ impl std::str::FromStr for Status { pub struct Item { /// The StatusNotifierItem that is wrapped by this instance. pub sni: proxy::StatusNotifierItemProxy<'static>, + gtk_menu: Option, } impl Item { @@ -68,7 +69,7 @@ impl Item { let sni = proxy::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,11 +81,20 @@ impl Item { } } - /// Get the menu of this item. - pub async fn menu(&self) -> zbus::Result { - // TODO document what this returns if there is no menu. + pub async fn set_menu(&mut self, widget: >k::EventBox) -> zbus::Result<()> { let menu = dbusmenu_gtk3::Menu::new(self.sni.destination(), &self.sni.menu().await?); - Ok(menu.upcast()) + menu.set_attach_widget(Some(widget)); + self.gtk_menu = Some(menu); + Ok(()) + } + + pub async fn popup_menu(&self, event: &gdk::EventButton, x: i32, y: i32) -> zbus::Result<()> { + if let Some(menu) = &self.gtk_menu { + menu.popup_at_pointer(event.downcast_ref::()); + Ok(()) + } else { + self.sni.context_menu(x, y).await + } } /// Get the current icon.