better working tray

This commit is contained in:
2026-01-02 16:50:02 +01:00
parent 2d5b492da8
commit ab7b3b3092
5 changed files with 559 additions and 228 deletions

View File

@@ -3,10 +3,145 @@
#include <gdkmm/rectangle.h>
#include <gio/gmenu.h>
#include <gtk/gtk.h>
#include <iostream>
#include <cmath>
#include <graphene.h>
#include <utility>
#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<float>(x), static_cast<float>(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<int32_t>(geom.x + std::lround(dst.x));
outY = static_cast<int32_t>(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<int32_t>(geom.x + std::lround(sx));
outY = static_cast<int32_t>(geom.y + std::lround(sy));
return true;
}
} // namespace
TrayIconWidget::TrayIconWidget( std::string id)
: Button(id), id(std::move(id)),
container(Gtk::Orientation::HORIZONTAL) {
@@ -48,16 +183,16 @@ TrayIconWidget::TrayIconWidget( std::string id)
}
void TrayIconWidget::update(const TrayService::Item &item) {
hasRemoteMenu = item.menuAvailable;
menuPopupPending = false;
menuRequestInFlight = false;
if (!item.menuAvailable) {
menuModel.reset();
menuActions.reset();
menuPopupPending = false;
if (menuChangedConnection.connected()) {
menuChangedConnection.disconnect();
}
if (menuPopover) {
menuPopover->insert_action_group("dbusmenu",
Glib::RefPtr<Gio::ActionGroup>());
menuPopover->insert_action_group(
"dbusmenu", Glib::RefPtr<Gio::ActionGroup>());
menuPopover->set_menu_model({});
menuPopover->unparent();
menuPopover.reset();
@@ -89,52 +224,81 @@ void TrayIconWidget::update(const TrayService::Item &item) {
}
void TrayIconWidget::on_primary_released(int /*n_press*/, double x, double y) {
service.activate(id, -1, -1);
// Intentionally no-op: some tray items (e.g. Spotify) misbehave when the
// host forwards primary clicks.
(void)x;
(void)y;
}
void TrayIconWidget::on_secondary_released(int /*n_press*/, double x,
double y) {
service.contextMenu(id, -1, -1);
pendingX = x;
pendingY = y;
if (!ensure_menu()) {
// Prefer dbusmenu popover when available.
if (hasRemoteMenu) {
menuPopupPending = true;
if (menuRequestInFlight) {
return;
}
menuRequestInFlight = true;
service.request_menu_layout(
id, sigc::mem_fun(*this, &TrayIconWidget::on_menu_layout_ready));
return;
}
pendingX = x;
pendingY = y;
menuPopupPending = true;
try_popup();
// No dbusmenu: defer to the item's own ContextMenu.
if (is_wayland_display(GTK_WIDGET(gobj()))) {
service.contextMenu(id, -1, -1);
return;
}
int32_t sendX = static_cast<int32_t>(std::lround(x));
int32_t sendY = static_cast<int32_t>(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);
}
service.contextMenu(id, sendX, sendY);
}
bool TrayIconWidget::ensure_menu() {
auto layoutOpt = service.get_menu_layout(id);
void TrayIconWidget::on_menu_layout_ready(
std::optional<TrayService::MenuNode> layoutOpt) {
menuRequestInFlight = false;
if (!menuPopupPending) {
return;
}
if (!layoutOpt) {
menuPopupPending = false;
// If dbusmenu layout fetch failed, fall back to ContextMenu.
if (is_wayland_display(GTK_WIDGET(gobj()))) {
service.contextMenu(id, -1, -1);
} else {
service.contextMenu(id, static_cast<int32_t>(std::lround(pendingX)),
static_cast<int32_t>(std::lround(pendingY)));
}
menuModel.reset();
menuActions.reset();
menuPopupPending = false;
if (menuChangedConnection.connected()) {
menuChangedConnection.disconnect();
}
if (menuPopover) {
remove_action_group("dbusmenu");
menuPopover->remove_action_group("dbusmenu");
menuPopover->set_menu_model({});
menuPopover->unparent();
menuPopover.reset();
}
return false;
return;
}
const auto &layout = *layoutOpt;
auto menu = Gio::Menu::create();
auto actions = Gio::SimpleActionGroup::create();
auto menu = Gio::Menu::create();
auto actions = Gio::SimpleActionGroup::create();
populate_menu_items(layout.children, menu, actions);
const auto itemCount = menu->get_n_items();
if (itemCount == 0) {
return false;
if (menu->get_n_items() == 0) {
menuModel.reset();
menuActions.reset();
menuPopupPending = false;
return;
}
menuModel = menu;
@@ -145,7 +309,8 @@ bool TrayIconWidget::ensure_menu() {
menuPopover =
Glib::make_refptr_for_instance<Gtk::PopoverMenu>(rawPopover);
if (!menuPopover) {
return false;
menuPopupPending = false;
return;
}
menuPopover->set_has_arrow(false);
@@ -155,36 +320,8 @@ bool TrayIconWidget::ensure_menu() {
menuPopover->remove_action_group("dbusmenu");
menuPopover->insert_action_group("dbusmenu", menuActions);
if (menuChangedConnection.connected()) {
menuChangedConnection.disconnect();
}
menuChangedConnection = menuModel->signal_items_changed().connect(
sigc::mem_fun(*this, &TrayIconWidget::on_menu_items_changed));
menuPopover->set_menu_model(menuModel);
return true;
}
void TrayIconWidget::on_menu_items_changed(guint /*position*/,
guint /*removed*/, guint /*added*/) {
if (!menuModel) {
return;
}
try_popup();
}
void TrayIconWidget::try_popup() {
if (!menuPopupPending || !menuPopover || !menuModel) {
return;
}
if (menuModel->get_n_items() == 0) {
return;
}
Gdk::Rectangle rect(static_cast<int>(pendingX), static_cast<int>(pendingY),
1, 1);
menuPopover->set_pointing_to(rect);