#include "widgets/tray.hpp" #include #include #include #include #include #include #include #include "components/base/button.hpp" namespace { bool is_wayland_display(GtkWidget *widget) { if (!widget) { return true; } GtkNative *native = gtk_widget_get_native(widget); if (!native) { return true; } GdkSurface *surface = gtk_native_get_surface(native); if (!surface) { return true; } GdkDisplay *display = gdk_surface_get_display(surface); if (!display) { return true; } const char *typeName = G_OBJECT_TYPE_NAME(display); if (!typeName) { return true; } return std::string(typeName).find("Wayland") != std::string::npos; } bool try_get_monitor_geometry(GtkWidget *widget, GdkRectangle &outGeom) { if (!widget) { return false; } GtkNative *native = gtk_widget_get_native(widget); if (!native) { return false; } GdkSurface *surface = gtk_native_get_surface(native); if (!surface) { return false; } GdkDisplay *display = gdk_surface_get_display(surface); if (!display) { return false; } GdkMonitor *monitor = gdk_display_get_monitor_at_surface(display, surface); if (!monitor) { return false; } gdk_monitor_get_geometry(monitor, &outGeom); return true; } bool try_get_global_click_coords(GtkWidget *widget, double x, double y, int32_t &outX, int32_t &outY) { if (!widget) { return false; } GtkNative *native = gtk_widget_get_native(widget); if (!native) { return false; } GtkWidget *nativeWidget = GTK_WIDGET(native); graphene_point_t src{static_cast(x), static_cast(y)}; graphene_point_t dst{0.0f, 0.0f}; if (!gtk_widget_compute_point(widget, nativeWidget, &src, &dst)) { return false; } GdkRectangle geom; if (!try_get_monitor_geometry(widget, geom)) { return false; } outX = static_cast(geom.x + std::lround(dst.x)); outY = static_cast(geom.y + std::lround(dst.y)); return true; } bool try_get_global_pointer_coords(GtkWidget *widget, int32_t &outX, int32_t &outY) { if (!widget) { return false; } GtkNative *native = gtk_widget_get_native(widget); if (!native) { return false; } GdkSurface *surface = gtk_native_get_surface(native); if (!surface) { return false; } GdkDisplay *display = gdk_surface_get_display(surface); if (!display) { return false; } GdkSeat *seat = gdk_display_get_default_seat(display); if (!seat) { return false; } GdkDevice *pointer = gdk_seat_get_pointer(seat); if (!pointer) { return false; } double sx = 0.0; double sy = 0.0; if (!gdk_surface_get_device_position(surface, pointer, &sx, &sy, nullptr)) { return false; } GdkRectangle geom; if (!try_get_monitor_geometry(widget, geom)) { return false; } outX = static_cast(geom.x + std::lround(sx)); outY = static_cast(geom.y + std::lround(sy)); return true; } bool has_popup_surface(GtkWidget *widget) { if (!widget) { return false; } GtkRoot *root = gtk_widget_get_root(widget); if (!root) { return false; } GtkNative *native = gtk_widget_get_native(widget); if (!native) { return false; } GdkSurface *surface = gtk_native_get_surface(native); if (!surface) { return false; } return gdk_surface_get_mapped(surface); } void log_menu_tree(const std::vector &nodes, int depth = 0) { const std::string indent(static_cast(depth) * 2, ' '); for (const auto &node : nodes) { if (!node.visible) { continue; } spdlog::debug( "[TrayIconWidget] menu node id={} label='{}' enabled={} sep={} depth={}", node.id, node.label, node.enabled ? 1 : 0, node.separator ? 1 : 0, depth); if (!node.children.empty()) { log_menu_tree(node.children, depth + 1); } } } } // namespace TrayIconWidget::TrayIconWidget( std::string id) : Button(id), id(std::move(id)), container(Gtk::Orientation::HORIZONTAL) { aliveFlag = std::make_shared(true); set_has_frame(false); set_focusable(false); set_valign(Gtk::Align::CENTER); set_halign(Gtk::Align::CENTER); add_css_class("tray-icon"); picture.set_halign(Gtk::Align::CENTER); picture.set_valign(Gtk::Align::CENTER); picture.set_can_shrink(true); picture.set_size_request(20, 20); image.set_pixel_size(20); image.set_halign(Gtk::Align::CENTER); image.set_valign(Gtk::Align::CENTER); container.set_halign(Gtk::Align::CENTER); container.set_valign(Gtk::Align::CENTER); container.append(picture); container.append(image); picture.set_visible(false); image.set_visible(true); set_child(container); primaryGesture = Gtk::GestureClick::create(); primaryGesture->set_button(GDK_BUTTON_PRIMARY); primaryGesture->signal_released().connect( sigc::mem_fun(*this, &TrayIconWidget::on_primary_released)); add_controller(primaryGesture); middleGesture = Gtk::GestureClick::create(); middleGesture->set_button(GDK_BUTTON_MIDDLE); middleGesture->signal_released().connect( sigc::mem_fun(*this, &TrayIconWidget::on_middle_released)); add_controller(middleGesture); secondaryGesture = Gtk::GestureClick::create(); secondaryGesture->set_button(GDK_BUTTON_SECONDARY); secondaryGesture->signal_released().connect( sigc::mem_fun(*this, &TrayIconWidget::on_secondary_released)); add_controller(secondaryGesture); } TrayIconWidget::~TrayIconWidget() { if (aliveFlag) { *aliveFlag = false; } if (menuPopover) { menuPopover->popdown(); menuPopover->remove_action_group("dbusmenu"); menuPopover->set_menu_model({}); if (menuPopover->get_parent()) { menuPopover->unparent(); } menuPopover.reset(); } } void TrayIconWidget::update(const TrayService::Item &item) { hasRemoteMenu = item.menuAvailable; menuPopupPending = false; menuRequestInFlight = false; if (!item.menuAvailable) { menuModel.reset(); menuActions.reset(); if (menuPopover) { menuPopover->insert_action_group( "dbusmenu", Glib::RefPtr()); menuPopover->set_menu_model({}); if (menuPopover->get_parent()) { menuPopover->unparent(); } menuPopover.reset(); } } if (item.iconPaintable) { picture.set_paintable(item.iconPaintable); picture.set_visible(true); image.set_visible(false); } else if (!item.iconName.empty()) { image.set_from_icon_name(item.iconName); image.set_pixel_size(20); image.set_visible(true); picture.set_visible(false); } else { picture.set_paintable({}); image.set_visible(false); picture.set_visible(false); } if (!item.title.empty()) { set_tooltip_text(item.title); } else { set_tooltip_text(""); } set_sensitive(item.status != "Passive"); } void TrayIconWidget::on_primary_released(int /*n_press*/, double x, double y) { int32_t sendX = static_cast(std::lround(x)); int32_t sendY = static_cast(std::lround(y)); // Try the most accurate coordinates first; fall back to pointer and finally // to -1/-1 so apps (e.g. Spotify) see a valid activate event on both // Wayland and X11. if (!try_get_global_click_coords(GTK_WIDGET(gobj()), x, y, sendX, sendY)) { if (!try_get_global_pointer_coords(GTK_WIDGET(gobj()), sendX, sendY)) { sendX = -1; sendY = -1; } } spdlog::debug("[TrayIconWidget] Activate primary id={} x={} y={}", id, sendX, sendY); service.activate(id, sendX, sendY); } void TrayIconWidget::on_middle_released(int /*n_press*/, double x, double y) { // Map middle click to the StatusNotifier SecondaryActivate event; some // apps (e.g. media players) use this for alternate actions like toggling // visibility. int32_t sendX = static_cast(std::lround(x)); int32_t sendY = static_cast(std::lround(y)); if (!try_get_global_click_coords(GTK_WIDGET(gobj()), x, y, sendX, sendY)) { if (!try_get_global_pointer_coords(GTK_WIDGET(gobj()), sendX, sendY)) { sendX = -1; sendY = -1; } } spdlog::debug( "[TrayIconWidget] SecondaryActivate (middle) id={} x={} y={}", id, sendX, sendY); service.secondaryActivate(id, sendX, sendY); } void TrayIconWidget::on_secondary_released(int /*n_press*/, double x, double y) { // If we are not attached to a toplevel (e.g., window hidden), fall back to // the item's own ContextMenu instead of trying to show a popover, which // would crash without a mapped surface. GtkWidget *selfWidget = GTK_WIDGET(gobj()); if (!gtk_widget_get_mapped(selfWidget) || !has_popup_surface(selfWidget)) { spdlog::debug( "[TrayIconWidget] Secondary fallback ContextMenu (no surface) id={}", id); service.contextMenu(id, -1, -1); return; } pendingX = x; pendingY = y; // Use dbusmenu popover when available and we have a mapped surface; else // fall back to the item's ContextMenu. if (hasRemoteMenu && has_popup_surface(selfWidget)) { spdlog::debug("[TrayIconWidget] Requesting dbusmenu for id={}", id); menuPopupPending = true; if (menuRequestInFlight) { return; } menuRequestInFlight = true; auto weak = std::weak_ptr(aliveFlag); service.request_menu_layout( id, [weak, this](std::optional layout) { if (auto locked = weak.lock()) { if (*locked) { on_menu_layout_ready(std::move(layout)); } } }); return; } int32_t sendX = static_cast(std::lround(x)); int32_t sendY = static_cast(std::lround(y)); if (!try_get_global_click_coords(GTK_WIDGET(gobj()), x, y, sendX, sendY)) { (void)try_get_global_pointer_coords(GTK_WIDGET(gobj()), sendX, sendY); } if (is_wayland_display(GTK_WIDGET(gobj()))) { spdlog::debug( "[TrayIconWidget] ContextMenu wayland id={} x=-1 y=-1", id); service.contextMenu(id, -1, -1); } else { spdlog::debug("[TrayIconWidget] ContextMenu id={} x={} y={}", id, sendX, sendY); service.contextMenu(id, sendX, sendY); } } void TrayIconWidget::on_menu_layout_ready( std::optional layoutOpt) { menuRequestInFlight = false; if (!menuPopupPending) { return; } GtkWidget *selfWidget = GTK_WIDGET(gobj()); if (!has_popup_surface(selfWidget)) { menuPopupPending = false; menuModel.reset(); menuActions.reset(); return; } if (!layoutOpt) { menuPopupPending = false; return; } const auto &layout = *layoutOpt; log_menu_tree(layout.children, 0); auto menu = Gio::Menu::create(); auto actions = Gio::SimpleActionGroup::create(); populate_menu_items(layout.children, menu, actions); if (menu->get_n_items() == 0) { menuModel.reset(); menuActions.reset(); menuPopupPending = false; return; } menuModel = menu; menuActions = actions; if (!menuPopover) { auto *rawPopover = Gtk::make_managed(); menuPopover = Glib::make_refptr_for_instance(rawPopover); if (!menuPopover) { menuPopupPending = false; return; } menuPopover->set_has_arrow(false); menuPopover->set_autohide(true); menuPopover->set_parent(*this); } menuPopover->remove_action_group("dbusmenu"); menuPopover->insert_action_group("dbusmenu", menuActions); menuPopover->set_menu_model(menuModel); // Ensure popover is still parented to us and has a native/root before popup. if (!menuPopover->get_parent()) { menuPopover->set_parent(*this); } GtkWidget *popoverWidget = GTK_WIDGET(menuPopover->gobj()); if (!popoverWidget || !gtk_widget_get_root(popoverWidget) || !gtk_widget_get_native(popoverWidget) || !has_popup_surface(selfWidget)) { menuPopupPending = false; return; } Gdk::Rectangle rect(static_cast(pendingX), static_cast(pendingY), 1, 1); menuPopover->set_pointing_to(rect); menuPopover->popup(); menuPopupPending = false; } void TrayIconWidget::populate_menu_items( const std::vector &nodes, const Glib::RefPtr &menu, const Glib::RefPtr &actions) { for (const auto &node : nodes) { if (!node.visible) { continue; } if (node.separator) { auto section = Gio::Menu::create(); menu->append_section("", section); continue; } if (!node.children.empty()) { auto submenu = Gio::Menu::create(); populate_menu_items(node.children, submenu, actions); auto submenuItem = Gio::MenuItem::create(node.label, Glib::ustring()); submenuItem->set_submenu(submenu); if (!node.enabled) { submenuItem->set_attribute_value( "enabled", Glib::Variant::create(false)); } menu->append_item(submenuItem); continue; } const std::string actionName = "item" + std::to_string(node.id); auto menuItem = Gio::MenuItem::create(node.label, "dbusmenu." + actionName); if (!node.enabled) { menuItem->set_attribute_value("enabled", Glib::Variant::create(false)); } auto action = Gio::SimpleAction::create(actionName); action->set_enabled(node.enabled); action->signal_activate().connect(sigc::bind( sigc::mem_fun(*this, &TrayIconWidget::on_menu_action), node.id)); actions->add_action(action); menu->append_item(menuItem); } } void TrayIconWidget::on_menu_action(const Glib::VariantBase & /*parameter*/, int itemId) { // Pop down immediately so the popover doesn't outlive us if the item // removes itself synchronously (e.g., "Exit"), which would otherwise lead // to use-after-free. if (menuPopover) { menuPopover->popdown(); // Also detach to avoid double-unparent if the item disappears during // the ensuing D-Bus call. if (menuPopover->get_parent()) { menuPopover->unparent(); } } int32_t sendX = -1; int32_t sendY = -1; (void)try_get_pending_coords(sendX, sendY); spdlog::debug("[TrayIconWidget] Menu action id={} item={} x={} y={}", this->id, itemId, sendX, sendY); const uint32_t nowMs = static_cast(g_get_monotonic_time() / 1000); // Use button 1 for menu activation events; some dbusmenu handlers ignore // secondary-button payloads for activate. service.activate_menu_item(id, itemId, sendX, sendY, 1 /*button*/, nowMs); } bool TrayIconWidget::try_get_pending_coords(int32_t &outX, int32_t &outY) const { outX = -1; outY = -1; int32_t sendX = static_cast(std::lround(pendingX)); int32_t sendY = static_cast(std::lround(pendingY)); if (!try_get_global_click_coords(GTK_WIDGET(gobj()), pendingX, pendingY, sendX, sendY)) { if (!try_get_global_pointer_coords(GTK_WIDGET(gobj()), sendX, sendY)) { sendX = -1; sendY = -1; } } outX = sendX; outY = sendY; return (sendX != -1 || sendY != -1); } TrayWidget::TrayWidget() : Gtk::Box(Gtk::Orientation::HORIZONTAL) { set_valign(Gtk::Align::CENTER); set_halign(Gtk::Align::CENTER); set_visible(false); addConnection = service->signal_item_added().connect( sigc::mem_fun(*this, &TrayWidget::on_item_added)); removeConnection = service->signal_item_removed().connect( sigc::mem_fun(*this, &TrayWidget::on_item_removed)); updateConnection = service->signal_item_updated().connect( sigc::mem_fun(*this, &TrayWidget::on_item_updated)); rebuild_existing(); } TrayWidget::~TrayWidget() { if (addConnection.connected()) { addConnection.disconnect(); } if (removeConnection.connected()) { removeConnection.disconnect(); } if (updateConnection.connected()) { updateConnection.disconnect(); } } void TrayWidget::rebuild_existing() { auto items = service->snapshotItems(); for (const auto &item : items) { on_item_added(item); } set_visible(!icons.empty()); } void TrayWidget::on_item_added(const TrayService::Item &item) { auto it = icons.find(item.id); if (it != icons.end()) { it->second->update(item); return; } auto icon = std::make_unique(item.id); icon->update(item); auto *raw = icon.get(); append(*raw); icons.emplace(item.id, std::move(icon)); set_visible(true); } void TrayWidget::on_item_removed(const std::string &id) { auto it = icons.find(id); if (it == icons.end()) { return; } remove(*it->second); icons.erase(it); if (icons.empty()) { set_visible(false); } } void TrayWidget::on_item_updated(const TrayService::Item &item) { auto it = icons.find(item.id); if (it == icons.end()) { on_item_added(item); return; } it->second->update(item); }