quick commit

This commit is contained in:
2026-01-03 22:55:02 +01:00
parent ab7b3b3092
commit 0e613141da
4 changed files with 227 additions and 42 deletions

View File

@@ -6,6 +6,7 @@
#include <cmath>
#include <graphene.h>
#include <utility>
#include <iostream>
#include "components/base/button.hpp"
namespace {
@@ -140,11 +141,49 @@ bool try_get_global_pointer_coords(GtkWidget *widget, int32_t &outX,
outY = static_cast<int32_t>(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<TrayService::MenuNode> &nodes,
int depth = 0) {
const std::string indent(static_cast<std::size_t>(depth) * 2, ' ');
for (const auto &node : nodes) {
if (!node.visible) {
continue;
}
std::cerr << "[TrayIconWidget] menu node id=" << node.id
<< " label='" << node.label << "' enabled="
<< (node.enabled ? "1" : "0") << " sep="
<< (node.separator ? "1" : "0") << " depth=" << depth
<< std::endl;
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<bool>(true);
set_has_frame(false);
set_focusable(false);
set_valign(Gtk::Align::CENTER);
@@ -175,6 +214,12 @@ TrayIconWidget::TrayIconWidget( std::string id)
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(
@@ -182,6 +227,21 @@ TrayIconWidget::TrayIconWidget( std::string id)
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;
@@ -194,7 +254,9 @@ void TrayIconWidget::update(const TrayService::Item &item) {
menuPopover->insert_action_group(
"dbusmenu", Glib::RefPtr<Gio::ActionGroup>());
menuPopover->set_menu_model({});
menuPopover->unparent();
if (menuPopover->get_parent()) {
menuPopover->unparent();
}
menuPopover.reset();
}
}
@@ -224,33 +286,78 @@ void TrayIconWidget::update(const TrayService::Item &item) {
}
void TrayIconWidget::on_primary_released(int /*n_press*/, double x, double y) {
// Intentionally no-op: some tray items (e.g. Spotify) misbehave when the
// host forwards primary clicks.
(void)x;
(void)y;
int32_t sendX = static_cast<int32_t>(std::lround(x));
int32_t sendY = static_cast<int32_t>(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;
}
}
std::cerr << "[TrayIconWidget] Activate primary id=" << id << " x="
<< sendX << " y=" << sendY << std::endl;
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<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)) {
if (!try_get_global_pointer_coords(GTK_WIDGET(gobj()), sendX, sendY)) {
sendX = -1;
sendY = -1;
}
}
std::cerr << "[TrayIconWidget] SecondaryActivate (middle) id=" << id
<< " x=" << sendX << " y=" << sendY << std::endl;
service.secondaryActivate(id, sendX, sendY);
}
void TrayIconWidget::on_secondary_released(int /*n_press*/, double x,
double y) {
pendingX = x;
pendingY = 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)) {
std::cerr << "[TrayIconWidget] Secondary fallback ContextMenu (no surface) id="
<< id << std::endl;
service.contextMenu(id, -1, -1);
return;
}
// Prefer dbusmenu popover when available.
if (hasRemoteMenu) {
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)) {
std::cerr << "[TrayIconWidget] Requesting dbusmenu for id=" << id
<< std::endl;
menuPopupPending = true;
if (menuRequestInFlight) {
return;
}
menuRequestInFlight = true;
auto weak = std::weak_ptr<bool>(aliveFlag);
service.request_menu_layout(
id, sigc::mem_fun(*this, &TrayIconWidget::on_menu_layout_ready));
return;
}
// No dbusmenu: defer to the item's own ContextMenu.
if (is_wayland_display(GTK_WIDGET(gobj()))) {
service.contextMenu(id, -1, -1);
id, [weak, this](std::optional<TrayService::MenuNode> layout) {
if (auto locked = weak.lock()) {
if (*locked) {
on_menu_layout_ready(std::move(layout));
}
}
});
return;
}
@@ -259,7 +366,15 @@ void TrayIconWidget::on_secondary_released(int /*n_press*/, double x,
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);
if (is_wayland_display(GTK_WIDGET(gobj()))) {
std::cerr << "[TrayIconWidget] ContextMenu wayland id=" << id
<< " x=-1 y=-1" << std::endl;
service.contextMenu(id, -1, -1);
} else {
std::cerr << "[TrayIconWidget] ContextMenu id=" << id << " x=" << sendX
<< " y=" << sendY << std::endl;
service.contextMenu(id, sendX, sendY);
}
}
void TrayIconWidget::on_menu_layout_ready(
@@ -270,25 +385,22 @@ void TrayIconWidget::on_menu_layout_ready(
return;
}
if (!layoutOpt) {
GtkWidget *selfWidget = GTK_WIDGET(gobj());
if (!has_popup_surface(selfWidget)) {
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();
if (menuPopover) {
menuPopover->remove_action_group("dbusmenu");
menuPopover->set_menu_model({});
}
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();
@@ -322,6 +434,18 @@ void TrayIconWidget::on_menu_layout_ready(
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<int>(pendingX), static_cast<int>(pendingY),
1, 1);
menuPopover->set_pointing_to(rect);
@@ -378,10 +502,48 @@ void TrayIconWidget::populate_menu_items(
void TrayIconWidget::on_menu_action(const Glib::VariantBase & /*parameter*/,
int itemId) {
service.activate_menu_item(id, 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);
std::cerr << "[TrayIconWidget] Menu action id=" << this->id
<< " item=" << itemId << " x=" << sendX << " y=" << sendY
<< std::endl;
const uint32_t nowMs = static_cast<uint32_t>(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<int32_t>(std::lround(pendingX));
int32_t sendY = static_cast<int32_t>(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()
@@ -444,7 +606,6 @@ void TrayWidget::on_item_removed(const std::string &id) {
}
remove(*it->second);
it->second->unparent();
icons.erase(it);
if (icons.empty()) {