Skip to content

Commit

Permalink
systray: replace MenuBar/MenuItem with Box/EventBox
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
moetayuko committed Jan 1, 2024
1 parent 350fb7a commit 8cebce2
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 51 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 32 additions & 30 deletions crates/eww/src/widgets/systray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,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 {
Expand All @@ -78,30 +78,30 @@ pub fn spawn_systray(menubar: &gtk::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();
});
}

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);
}
Expand All @@ -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<glib::JoinHandle<()>>,
Expand All @@ -127,7 +127,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 {
Expand All @@ -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<i32>,
) -> zbus::Result<()> {
// init icon
Expand All @@ -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
Expand All @@ -175,7 +174,7 @@ impl Item {
let window =
widget.toplevel().expect("Failed to obtain toplevel window").downcast::<Window>().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();
Expand All @@ -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)
}));
Expand Down
25 changes: 9 additions & 16 deletions crates/eww/src/widgets/widget_definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1047,13 +1047,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 {
Expand All @@ -1062,8 +1067,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);
Expand Down Expand Up @@ -1125,16 +1128,6 @@ fn parse_justification(j: &str) -> Result<gtk::Justification> {
}
}

/// @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));
Expand Down
1 change: 1 addition & 0 deletions crates/notifier_host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
21 changes: 16 additions & 5 deletions crates/notifier_host/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,15 @@ fn split_service_name(service: &str) -> zbus::Result<(String, String)> {

pub struct Item {
pub sni: dbus::StatusNotifierItemProxy<'static>,
gtk_menu: Option<dbusmenu_gtk3::Menu>,
}

impl Item {
pub async fn from_address(con: &zbus::Connection, addr: &str) -> zbus::Result<Self> {
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.
Expand All @@ -80,10 +81,20 @@ impl Item {
}
}

pub async fn menu(&self) -> zbus::Result<gtk::Menu> {
// 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: &gtk::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::<gdk::Event>());
Ok(())
} else {
self.sni.context_menu(x, y).await
}
}

pub async fn icon(&self, size: i32, scale: i32) -> Option<gtk::gdk_pixbuf::Pixbuf> {
Expand Down

0 comments on commit 8cebce2

Please sign in to comment.