From e74dd6fa45f3a01d8cc70cd080ad4dd58aae37d1 Mon Sep 17 00:00:00 2001
From: MoetaYuko <loli@yuko.moe>
Date: Sat, 30 Dec 2023 21:38:55 +0800
Subject: [PATCH] systray: replace MenuBar/MenuItem with Box/EventBox

The major benefit of MenuItem is automatic handling of context menus.
However, MenuItem cannot properly process right mouse click, making it
less useful. Hence, this patch replaces it (as long as the container)
with a simple EventBox and process button clicks on our own.
---
 Cargo.lock                                   |  1 +
 crates/eww/src/widgets/systray.rs            | 60 ++++++++++----------
 crates/eww/src/widgets/widget_definitions.rs | 25 +++-----
 crates/notifier_host/Cargo.toml              |  1 +
 crates/notifier_host/src/item.rs             | 20 +++++--
 5 files changed, 57 insertions(+), 50 deletions(-)

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<String, Item>,
 
     icon_size: tokio::sync::watch::Receiver<i32>,
 }
 
-pub fn spawn_systray(menubar: &gtk::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: &gtk::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: &gtk::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: &gtk::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<glib::JoinHandle<()>>,
@@ -119,7 +119,7 @@ impl Drop for Item {
 
 impl Item {
     fn new(id: String, item: notifier_host::Item, icon_size: tokio::sync::watch::Receiver<i32>) -> 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<i32>,
     ) -> 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<super::graph::Graph> {
 const WIDGET_NAME_SYSTRAY: &str = "systray";
 /// @widget systray
 /// @desc Tray for system notifier icons
-fn build_systray(bargs: &mut BuilderArgs) -> Result<gtk::MenuBar> {
-    let gtk_widget = gtk::MenuBar::new();
+fn build_systray(bargs: &mut BuilderArgs) -> Result<gtk::Box> {
+    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<gtk::MenuBar> {
                 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(&gtk_widget, &props_clone);
@@ -1239,16 +1242,6 @@ fn parse_gravity(g: &str) -> Result<gtk::pango::Gravity> {
     }
 }
 
-/// @var pack-direction - "right", "ltr", "left", "rtl", "down", "ttb", "up", "btt"
-fn parse_packdirection(o: &str) -> Result<gtk::PackDirection> {
-    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<W: IsA<gtk::Widget>, 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<dbusmenu_gtk3::Menu>,
 }
 
 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<gtk::Menu> {
-        // TODO document what this returns if there is no menu.
+    pub async fn set_menu(&mut self, widget: &gtk::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::<gdk::Event>());
+            Ok(())
+        } else {
+            self.sni.context_menu(x, y).await
+        }
     }
 
     /// Get the current icon.