Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle mouse clicks for systray items #6

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

23 changes: 13 additions & 10 deletions crates/eww/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
paths::EwwPaths,
script_var_handler::ScriptVarHandlerHandle,
state::scope_graph::{ScopeGraph, ScopeIndex},
widgets::window::Window,
window_arguments::WindowArguments,
window_initiator::WindowInitiator,
*,
Expand Down Expand Up @@ -92,7 +93,7 @@ pub struct EwwWindow {
pub instance_id: String,
pub name: String,
pub scope_index: ScopeIndex,
pub gtk_window: gtk::Window,
pub gtk_window: Window,
pub destroy_event_handler_id: Option<glib::SignalHandlerId>,
}

Expand Down Expand Up @@ -524,15 +525,21 @@ fn initialize_window<B: DisplayBackend>(
window_scope: ScopeIndex,
) -> Result<EwwWindow> {
let monitor_geometry = monitor.geometry();
let window = B::initialize_window(window_init, monitor_geometry)
let (actual_window_rect, x, y) = match window_init.geometry {
Some(geometry) => {
let rect = get_window_rectangle(geometry, monitor_geometry);
(Some(rect), rect.x(), rect.y())
}
_ => (None, 0, 0),
};
let window = B::initialize_window(window_init, monitor_geometry, x, y)
.with_context(|| format!("monitor {} is unavailable", window_init.monitor.clone().unwrap()))?;

window.set_title(&format!("Eww - {}", window_init.name));
window.set_position(gtk::WindowPosition::None);
window.set_gravity(gdk::Gravity::Center);

if let Some(geometry) = window_init.geometry {
let actual_window_rect = get_window_rectangle(geometry, monitor_geometry);
if let Some(actual_window_rect) = actual_window_rect {
window.set_size_request(actual_window_rect.width(), actual_window_rect.height());
window.set_default_size(actual_window_rect.width(), actual_window_rect.height());
}
Expand Down Expand Up @@ -575,11 +582,7 @@ fn initialize_window<B: DisplayBackend>(

/// Apply the provided window-positioning rules to the window.
#[cfg(feature = "x11")]
fn apply_window_position(
mut window_geometry: WindowGeometry,
monitor_geometry: gdk::Rectangle,
window: &gtk::Window,
) -> Result<()> {
fn apply_window_position(mut window_geometry: WindowGeometry, monitor_geometry: gdk::Rectangle, window: &Window) -> Result<()> {
let gdk_window = window.window().context("Failed to get gdk window from gtk window")?;
window_geometry.size = Coords::from_pixels(window.size());
let actual_window_rect = get_window_rectangle(window_geometry, monitor_geometry);
Expand All @@ -593,7 +596,7 @@ fn apply_window_position(
Ok(())
}

fn on_screen_changed(window: &gtk::Window, _old_screen: Option<&gdk::Screen>) {
fn on_screen_changed(window: &Window, _old_screen: Option<&gdk::Screen>) {
let visual = gtk::prelude::GtkWindowExt::screen(window)
.and_then(|screen| screen.rgba_visual().filter(|_| screen.is_composited()).or_else(|| screen.system_visual()));
window.set_visual(visual.as_ref());
Expand Down
24 changes: 12 additions & 12 deletions crates/eww/src/display_backend.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::window_initiator::WindowInitiator;
use crate::{widgets::window::Window, window_initiator::WindowInitiator};

#[cfg(feature = "wayland")]
pub use platform_wayland::WaylandBackend;
Expand All @@ -9,22 +9,22 @@ pub use platform_x11::{set_xprops, X11Backend};
pub trait DisplayBackend: Send + Sync + 'static {
const IS_X11: bool;

fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle) -> Option<gtk::Window>;
fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window>;
}

pub struct NoBackend;

impl DisplayBackend for NoBackend {
const IS_X11: bool = false;

fn initialize_window(_window_init: &WindowInitiator, _monitor: gdk::Rectangle) -> Option<gtk::Window> {
Some(gtk::Window::new(gtk::WindowType::Toplevel))
fn initialize_window(_window_init: &WindowInitiator, _monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> {
Some(Window::new(gtk::WindowType::Toplevel, x, y))
}
}

#[cfg(feature = "wayland")]
mod platform_wayland {
use crate::window_initiator::WindowInitiator;
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
use gtk::prelude::*;
use yuck::config::{window_definition::WindowStacking, window_geometry::AnchorAlignment};

Expand All @@ -35,8 +35,8 @@ mod platform_wayland {
impl DisplayBackend for WaylandBackend {
const IS_X11: bool = false;

fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle) -> Option<gtk::Window> {
let window = gtk::Window::new(gtk::WindowType::Toplevel);
fn initialize_window(window_init: &WindowInitiator, monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> {
let window = Window::new(gtk::WindowType::Toplevel, x, y);
// Initialising a layer shell surface
gtk_layer_shell::init_for_window(&window);
// Sets the monitor where the surface is shown
Expand Down Expand Up @@ -112,7 +112,7 @@ mod platform_wayland {

#[cfg(feature = "x11")]
mod platform_x11 {
use crate::window_initiator::WindowInitiator;
use crate::{widgets::window::Window, window_initiator::WindowInitiator};
use anyhow::{Context, Result};
use gdk::Monitor;
use gtk::{self, prelude::*};
Expand All @@ -135,10 +135,10 @@ mod platform_x11 {
impl DisplayBackend for X11Backend {
const IS_X11: bool = true;

fn initialize_window(window_init: &WindowInitiator, _monitor: gdk::Rectangle) -> Option<gtk::Window> {
fn initialize_window(window_init: &WindowInitiator, _monitor: gdk::Rectangle, x: i32, y: i32) -> Option<Window> {
let window_type =
if window_init.backend_options.x11.wm_ignore { gtk::WindowType::Popup } else { gtk::WindowType::Toplevel };
let window = gtk::Window::new(window_type);
let window = Window::new(window_type, x, y);
window.set_resizable(window_init.resizable);
window.set_keep_above(window_init.stacking == WindowStacking::Foreground);
window.set_keep_below(window_init.stacking == WindowStacking::Background);
Expand All @@ -151,7 +151,7 @@ mod platform_x11 {
}
}

pub fn set_xprops(window: &gtk::Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> {
pub fn set_xprops(window: &Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> {
let backend = X11BackendConnection::new()?;
backend.set_xprops_for(window, monitor, window_init)?;
Ok(())
Expand All @@ -171,7 +171,7 @@ mod platform_x11 {
Ok(X11BackendConnection { conn, root_window: screen.root, atoms })
}

fn set_xprops_for(&self, window: &gtk::Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> {
fn set_xprops_for(&self, window: &Window, monitor: Monitor, window_init: &WindowInitiator) -> Result<()> {
let monitor_rect = monitor.geometry();
let scale_factor = monitor.scale_factor() as u32;
let gdk_window = window.window().context("Couldn't get gdk window from gtk window")?;
Expand Down
1 change: 1 addition & 0 deletions crates/eww/src/widgets/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod graph;
mod systray;
pub mod transform;
pub mod widget_definitions;
pub mod window;

/// Run a command that was provided as an attribute.
/// This command may use placeholders which will be replaced by the values of the arguments given.
Expand Down
94 changes: 77 additions & 17 deletions crates/eww/src/widgets/systray.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use crate::widgets::window::Window;
use futures::StreamExt;
use gtk::{cairo::Surface, gdk::ffi::gdk_cairo_surface_create_from_pixbuf, prelude::*};
use notifier_host;
use std::{cell::RefCell, future::Future, rc::Rc};

// DBus state shared between systray instances, to avoid creating too many connections etc.
struct DBusSession {
Expand All @@ -23,14 +25,20 @@ async fn dbus_session() -> zbus::Result<&'static DBusSession> {
.await
}

fn run_async_task<F: Future>(f: F) -> F::Output {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().expect("Failed to initialize tokio runtime");
rt.block_on(f)
}

pub struct Props {
icon_size_tx: tokio::sync::watch::Sender<i32>,
pub prepend_new: Rc<RefCell<bool>>,
}

impl Props {
pub fn new() -> Self {
let (icon_size_tx, _) = tokio::sync::watch::channel(24);
Self { icon_size_tx }
Self { icon_size_tx, prepend_new: Rc::new(RefCell::new(false)) }
}

pub fn icon_size(&self, value: i32) {
Expand All @@ -46,14 +54,20 @@ impl Props {
}

struct Tray {
menubar: gtk::MenuBar,
container: gtk::Box,
items: std::collections::HashMap<String, Item>,

icon_size: tokio::sync::watch::Receiver<i32>,
prepend_new: Rc<RefCell<bool>>,
}

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(),
prepend_new: props.prepend_new.clone(),
};

let task = glib::MainContext::default().spawn_local(async move {
let s = match dbus_session().await {
Expand All @@ -64,29 +78,33 @@ 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();
});
}

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);
if *self.prepend_new.borrow() {
self.container.pack_end(&item.widget, true, true, 0);
} else {
self.container.pack_start(&item.widget, true, true, 0);
}
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 @@ -96,7 +114,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 @@ -112,7 +130,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 @@ -125,8 +143,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 @@ -135,9 +153,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
Expand All @@ -156,9 +173,52 @@ impl Item {
let scale = icon.scale_factor();
load_icon_for_item(&icon, &item, *icon_size.borrow_and_update(), scale).await;

let item = Rc::new(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| {
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();
let item_is_menu = item_is_menu.unwrap_or(false);
log::debug!(
"mouse click button={}, x={}, y={}, have_item_is_menu={}, item_is_menu={}",
evt.button(),
x,
y,
have_item_is_menu,
item_is_menu
);

let result = match (evt.button(), item_is_menu) {
(gdk::BUTTON_PRIMARY, 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, _) => 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 })
}
_ => 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)
}));

// updates
let mut status_updates = item.sni.receive_new_status().await?;
let mut title_updates = item.sni.receive_new_status().await?;
let mut title_updates = item.sni.receive_new_title().await?;
let mut icon_updates = item.sni.receive_new_icon().await?;

loop {
Expand Down
Loading