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.
This commit is contained in:
MoetaYuko 2023-12-30 21:38:55 +08:00 committed by ElKowar
parent 361b8d1b66
commit e74dd6fa45
5 changed files with 57 additions and 50 deletions

1
Cargo.lock generated
View file

@ -1954,6 +1954,7 @@ name = "notifier_host"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"dbusmenu-gtk3", "dbusmenu-gtk3",
"gdk",
"gtk", "gtk",
"log", "log",
"thiserror", "thiserror",

View file

@ -53,14 +53,14 @@ impl Props {
} }
struct Tray { struct Tray {
menubar: gtk::MenuBar, container: gtk::Box,
items: std::collections::HashMap<String, Item>, items: std::collections::HashMap<String, Item>,
icon_size: tokio::sync::watch::Receiver<i32>, icon_size: tokio::sync::watch::Receiver<i32>,
} }
pub fn spawn_systray(menubar: &gtk::MenuBar, props: &Props) { pub fn spawn_systray(container: &gtk::Box, props: &Props) {
let mut systray = Tray { menubar: menubar.clone(), items: Default::default(), icon_size: props.icon_size_tx.subscribe() }; 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 task = glib::MainContext::default().spawn_local(async move {
let s = match dbus_session().await { 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; let e = notifier_host::run_host(&mut systray, &s.snw).await;
log::error!("notifier host error: {}", e); log::error!("notifier host error: {}", e);
}); });
// stop the task when the widget is dropped // stop the task when the widget is dropped
menubar.connect_destroy(move |_| { container.connect_destroy(move |_| {
task.abort(); task.abort();
}); });
} }
@ -85,15 +85,15 @@ pub fn spawn_systray(menubar: &gtk::MenuBar, props: &Props) {
impl notifier_host::Host for Tray { impl notifier_host::Host for Tray {
fn add_item(&mut self, id: &str, item: notifier_host::Item) { fn add_item(&mut self, id: &str, item: notifier_host::Item) {
let item = Item::new(id.to_owned(), item, self.icon_size.clone()); 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) { 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) { fn remove_item(&mut self, id: &str) {
if let Some(item) = self.items.get(id) { if let Some(item) = self.items.get(id) {
self.menubar.remove(&item.widget); self.container.remove(&item.widget);
} else { } else {
log::warn!("Tried to remove nonexistent item {:?} from systray", id); 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. /// Item represents a single icon being shown in the system tray.
struct Item { struct Item {
/// Main widget representing this tray item. /// Main widget representing this tray item.
widget: gtk::MenuItem, widget: gtk::EventBox,
/// Async task to stop when this item gets removed. /// Async task to stop when this item gets removed.
task: Option<glib::JoinHandle<()>>, task: Option<glib::JoinHandle<()>>,
@ -119,7 +119,7 @@ impl Drop for Item {
impl Item { impl Item {
fn new(id: String, item: notifier_host::Item, icon_size: tokio::sync::watch::Receiver<i32>) -> Self { 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 out_widget = widget.clone(); // copy so we can return it
let task = glib::MainContext::default().spawn_local(async move { let task = glib::MainContext::default().spawn_local(async move {
@ -132,8 +132,8 @@ impl Item {
} }
async fn maintain( async fn maintain(
widget: gtk::MenuItem, widget: gtk::EventBox,
item: notifier_host::Item, mut item: notifier_host::Item,
mut icon_size: tokio::sync::watch::Receiver<i32>, mut icon_size: tokio::sync::watch::Receiver<i32>,
) -> zbus::Result<()> { ) -> zbus::Result<()> {
// init icon // init icon
@ -142,9 +142,8 @@ impl Item {
icon.show(); icon.show();
// init menu // init menu
match item.menu().await { if let Err(e) = item.set_menu(&widget).await {
Ok(m) => widget.set_submenu(Some(&m)), log::warn!("failed to set menu: {}", e);
Err(e) => log::warn!("failed to get menu: {}", e),
} }
// TODO this is a lot of code duplication unfortunately, i'm not really sure how to // 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 item_is_menu
); );
match (evt.button(), item_is_menu) { let result = match (evt.button(), item_is_menu) {
(gdk::BUTTON_PRIMARY, false) => { (gdk::BUTTON_PRIMARY, false) => {
if let Err(e) = run_async_task(async { item.sni.activate(x, y).await }) { let result = run_async_task(async { item.sni.activate(x, y).await });
log::error!("failed to send activate event: {}", e); if result.is_err() && !have_item_is_menu {
if !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) // Some applications are in fact menu-only (don't have Activate method)
// but don't report so through ItemIsMenu property. Fallback to menu if // but don't report so through ItemIsMenu property. Fallback to menu if
// activate failed in this case. // activate failed in this case.
return gtk::Inhibit(false); run_async_task(async { item.popup_menu( evt, x, y).await })
} } else {
result
} }
} }
(gdk::BUTTON_MIDDLE, _) => { (gdk::BUTTON_MIDDLE, _) => run_async_task(async { item.sni.secondary_activate(x, y).await }),
if let Err(e) = run_async_task(async { item.sni.secondary_activate(x, y).await }) { (gdk::BUTTON_SECONDARY, _) | (gdk::BUTTON_PRIMARY, true) => {
log::error!("failed to send secondary activate event: {}", e); 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) gtk::Inhibit(true)
})); }));

View file

@ -1138,13 +1138,18 @@ fn build_graph(bargs: &mut BuilderArgs) -> Result<super::graph::Graph> {
const WIDGET_NAME_SYSTRAY: &str = "systray"; const WIDGET_NAME_SYSTRAY: &str = "systray";
/// @widget systray /// @widget systray
/// @desc Tray for system notifier icons /// @desc Tray for system notifier icons
fn build_systray(bargs: &mut BuilderArgs) -> Result<gtk::MenuBar> { fn build_systray(bargs: &mut BuilderArgs) -> Result<gtk::Box> {
let gtk_widget = gtk::MenuBar::new(); let gtk_widget = gtk::Box::new(gtk::Orientation::Horizontal, 0);
let props = Rc::new(systray::Props::new()); 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, { 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 - size of icons in the tray
prop(icon_size: as_i32) { prop(icon_size: as_i32) {
if icon_size <= 0 { if icon_size <= 0 {
@ -1153,8 +1158,6 @@ fn build_systray(bargs: &mut BuilderArgs) -> Result<gtk::MenuBar> {
props.icon_size(icon_size); 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); 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. /// 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) { 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)); let signal_handler_id = std::rc::Rc::new(std::cell::RefCell::new(None));

View file

@ -10,6 +10,7 @@ homepage = "https://github.com/elkowar/eww"
[dependencies] [dependencies]
gtk = "0.17.1" gtk = "0.17.1"
gdk = "0.17.1"
zbus = { version = "3.7.0", default-features = false, features = ["tokio"] } zbus = { version = "3.7.0", default-features = false, features = ["tokio"] }
dbusmenu-gtk3 = "0.1.0" dbusmenu-gtk3 = "0.1.0"

View file

@ -42,6 +42,7 @@ impl std::str::FromStr for Status {
pub struct Item { pub struct Item {
/// The StatusNotifierItem that is wrapped by this instance. /// The StatusNotifierItem that is wrapped by this instance.
pub sni: proxy::StatusNotifierItemProxy<'static>, pub sni: proxy::StatusNotifierItemProxy<'static>,
gtk_menu: Option<dbusmenu_gtk3::Menu>,
} }
impl Item { impl Item {
@ -68,7 +69,7 @@ impl Item {
let sni = proxy::StatusNotifierItemProxy::builder(con).destination(addr)?.path(path)?.build().await?; 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. /// Get the current status of the item.
@ -80,11 +81,20 @@ impl Item {
} }
} }
/// Get the menu of this item. pub async fn set_menu(&mut self, widget: &gtk::EventBox) -> zbus::Result<()> {
pub async fn menu(&self) -> zbus::Result<gtk::Menu> {
// TODO document what this returns if there is no menu.
let menu = dbusmenu_gtk3::Menu::new(self.sni.destination(), &self.sni.menu().await?); 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. /// Get the current icon.