diff --git a/include/services/tray.hpp b/include/services/tray.hpp index 742f857..47f4ee0 100644 --- a/include/services/tray.hpp +++ b/include/services/tray.hpp @@ -50,7 +50,8 @@ class TrayService { bool separator = false; std::vector children; }; - std::optional get_menu_layout(const std::string &id); + using MenuLayoutCallback = sigc::slot)>; + void request_menu_layout(const std::string &id, MenuLayoutCallback callback); bool activate_menu_item(const std::string &id, int itemId); sigc::signal &signal_item_added(); @@ -75,6 +76,11 @@ class TrayService { guint ownerWatchId = 0; Glib::RefPtr menuModel; Glib::RefPtr menuActions; + + guint refreshSourceId = 0; + bool refreshInFlight = false; + bool refreshQueued = false; + bool addSignalPending = false; }; Glib::RefPtr connection; @@ -124,7 +130,11 @@ class TrayService { void register_item(const Glib::ustring &sender, const std::string &service); void unregister_item(const std::string &id); - void refresh_item(TrackedItem &item); + void schedule_refresh(const std::string &id); + void begin_refresh(const std::string &id); + static gboolean refresh_timeout_cb(gpointer user_data); + static void on_refresh_finished_static(GObject *source, GAsyncResult *res, + gpointer user_data); void emit_registered_items_changed(); Glib::Variant> diff --git a/include/widgets/tray.hpp b/include/widgets/tray.hpp index 5b80dac..c182493 100644 --- a/include/widgets/tray.hpp +++ b/include/widgets/tray.hpp @@ -13,7 +13,9 @@ #include #include #include +#include #include +#include #include "services/tray.hpp" #include "components/base/button.hpp" @@ -35,16 +37,15 @@ class TrayIconWidget : public Button { Glib::RefPtr menuPopover; Glib::RefPtr menuActions; Glib::RefPtr menuModel; - sigc::connection menuChangedConnection; bool menuPopupPending = false; + bool menuRequestInFlight = false; + bool hasRemoteMenu = false; double pendingX = 0.0; double pendingY = 0.0; void on_primary_released(int n_press, double x, double y); void on_secondary_released(int n_press, double x, double y); - bool ensure_menu(); - void on_menu_items_changed(guint position, guint removed, guint added); - void try_popup(); + void on_menu_layout_ready(std::optional layout); void populate_menu_items(const std::vector &nodes, const Glib::RefPtr &menu, diff --git a/src/services/hyprland.cpp b/src/services/hyprland.cpp index ed8b5f2..d28dc8f 100644 --- a/src/services/hyprland.cpp +++ b/src/services/hyprland.cpp @@ -1,5 +1,6 @@ #include "services/hyprland.hpp" +#include #include #include #include @@ -9,6 +10,7 @@ #include #include #include +#include #include #include "helpers/systemHelper.hpp" @@ -213,6 +215,22 @@ void HyprlandService::refresh_workspaces() { } } + std::string clientsOutput = SystemHelper::get_command_output(kClientsCommand); + auto clientsJson = nlohmann::json::parse(clientsOutput, nullptr, false); + std::unordered_set liveClientAddresses; + for (const auto &clientJson : clientsJson) { + const std::string addr = clientJson.value("address", ""); + if (addr.empty()) { + continue; + } + + if (addr.rfind("0x", 0) == 0) { + liveClientAddresses.insert(addr.substr(2)); + } else { + liveClientAddresses.insert(addr); + } + } + for (const auto &workspaceJson : workspacesJson) { const int workspaceId = workspaceJson.value("id", -1); auto workspaceStateIt = this->workspaces.find(workspaceId); @@ -230,6 +248,17 @@ void HyprlandService::refresh_workspaces() { workspaceState->active = true; } + // drop urgent flags for windows no longer reported by hyprctl clients + for (auto &[id, ws] : this->workspaces) { + auto &urgent = ws->urgentWindows; + auto newEnd = std::remove_if(urgent.begin(), urgent.end(), [&](const std::string &addr) { + return liveClientAddresses.find(addr) == liveClientAddresses.end(); + }); + if (newEnd != urgent.end()) { + urgent.erase(newEnd, urgent.end()); + } + } + workspaceStateChanged.emit(); } diff --git a/src/services/tray.cpp b/src/services/tray.cpp index ef675f0..dc9e7be 100644 --- a/src/services/tray.cpp +++ b/src/services/tray.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -21,6 +22,11 @@ constexpr const char *kItemInterface = "org.kde.StatusNotifierItem"; constexpr const char *kDBusPropertiesIface = "org.freedesktop.DBus.Properties"; constexpr const char *kDBusMenuInterface = "com.canonical.dbusmenu"; +constexpr int kDBusTimeoutMs = 1500; +constexpr int kDBusMenuTimeoutMs = 2000; +constexpr int kRefreshDebounceMs = 50; +constexpr int kAboutToShowTimeoutMs = 800; + const char *kWatcherIntrospection = R"( @@ -105,53 +111,43 @@ void call_about_to_show(const Glib::RefPtr &connection, return; } - GError *error = nullptr; - GVariant *result = g_dbus_connection_call_sync( - connection->gobj(), busName.c_str(), menuPath.c_str(), - kDBusMenuInterface, "AboutToShow", g_variant_new("(i)", id), nullptr, - G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); - if (result) { - g_variant_unref(result); - } - if (error) { - std::cerr << "[TrayService] AboutToShow failed for " << busName - << menuPath << " (" << id << "): " << error->message - << std::endl; - g_error_free(error); - } + g_dbus_connection_call(connection->gobj(), busName.c_str(), menuPath.c_str(), + kDBusMenuInterface, "AboutToShow", + g_variant_new("(i)", id), nullptr, + G_DBUS_CALL_FLAGS_NONE, kAboutToShowTimeoutMs, + nullptr, nullptr, nullptr); } -GVariant *call_get_layout(const Glib::RefPtr &connection, - const std::string &busName, - const std::string &menuPath) { - if (!connection) { - return nullptr; +struct SimpleCallData { + std::string debugLabel; + bool ignoreUnknownMethod = false; +}; + +void on_simple_call_finished(GObject *source, GAsyncResult *res, + gpointer user_data) { + std::unique_ptr data( + static_cast(user_data)); + + GError *error = nullptr; + GVariant *reply = + g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); + + if (reply) { + g_variant_unref(reply); } - GVariant *properties = create_property_list_variant(); - if (!properties) { - return nullptr; + if (!error) { + return; } - GVariant *params = g_variant_new("(ii@as)", 0, -1, properties); - g_variant_ref_sink(properties); - - GError *error = nullptr; - GVariant *result = g_dbus_connection_call_sync( - connection->gobj(), busName.c_str(), menuPath.c_str(), - kDBusMenuInterface, "GetLayout", params, nullptr, - G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); - - g_variant_unref(properties); - - if (error) { - std::cerr << "[TrayService] GetLayout failed for " << busName - << menuPath << ": " << error->message << std::endl; - g_error_free(error); - return nullptr; + const bool isUnknownMethod = + (error->domain == G_DBUS_ERROR && error->code == G_DBUS_ERROR_UNKNOWN_METHOD); + if (!(data && data->ignoreUnknownMethod && isUnknownMethod)) { + std::cerr << "[TrayService] " + << (data ? data->debugLabel : std::string("D-Bus call")) + << " failed: " << error->message << std::endl; } - - return result; + g_error_free(error); } void parse_menu_node(GVariant *tuple, TrayService::MenuNode &outNode) { @@ -308,6 +304,10 @@ void TrayService::start() { void TrayService::stop() { if (connection) { for (auto &pair : items) { + if (pair.second->refreshSourceId != 0) { + g_source_remove(pair.second->refreshSourceId); + pair.second->refreshSourceId = 0; + } if (pair.second->signalSubscriptionId != 0) { g_dbus_connection_signal_unsubscribe( connection->gobj(), pair.second->signalSubscriptionId); @@ -361,22 +361,14 @@ void TrayService::activate(const std::string &id, int32_t x, int32_t y) { return; } - GError *error = nullptr; - GVariant *result = g_dbus_connection_call_sync( + auto data = new SimpleCallData(); + data->debugLabel = "Activate(" + id + ")"; + data->ignoreUnknownMethod = false; + g_dbus_connection_call( connection->gobj(), it->second->publicData.busName.c_str(), it->second->publicData.objectPath.c_str(), kItemInterface, "Activate", - g_variant_new("(ii)", x, y), nullptr, G_DBUS_CALL_FLAGS_NONE, -1, - nullptr, &error); - - if (result) { - g_variant_unref(result); - } - - if (error) { - std::cerr << "[TrayService] Activate failed for " << id << ": " - << error->message << std::endl; - g_error_free(error); - } + g_variant_new("(ii)", x, y), nullptr, G_DBUS_CALL_FLAGS_NONE, + kDBusTimeoutMs, nullptr, &on_simple_call_finished, data); } void TrayService::secondaryActivate(const std::string &id, int32_t x, @@ -386,22 +378,15 @@ void TrayService::secondaryActivate(const std::string &id, int32_t x, return; } - GError *error = nullptr; - GVariant *result = g_dbus_connection_call_sync( + auto data = new SimpleCallData(); + data->debugLabel = "SecondaryActivate(" + id + ")"; + data->ignoreUnknownMethod = false; + g_dbus_connection_call( connection->gobj(), it->second->publicData.busName.c_str(), it->second->publicData.objectPath.c_str(), kItemInterface, "SecondaryActivate", g_variant_new("(ii)", x, y), nullptr, - G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); - - if (result) { - g_variant_unref(result); - } - - if (error) { - std::cerr << "[TrayService] SecondaryActivate failed for " << id << ": " - << error->message << std::endl; - g_error_free(error); - } + G_DBUS_CALL_FLAGS_NONE, kDBusTimeoutMs, nullptr, + &on_simple_call_finished, data); } void TrayService::contextMenu(const std::string &id, int32_t x, int32_t y) { @@ -410,25 +395,15 @@ void TrayService::contextMenu(const std::string &id, int32_t x, int32_t y) { return; } - GError *error = nullptr; - GVariant *result = g_dbus_connection_call_sync( + auto data = new SimpleCallData(); + data->debugLabel = "ContextMenu(" + id + ")"; + data->ignoreUnknownMethod = true; + g_dbus_connection_call( connection->gobj(), it->second->publicData.busName.c_str(), it->second->publicData.objectPath.c_str(), kItemInterface, "ContextMenu", g_variant_new("(ii)", x, y), nullptr, - G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); - - if (result) { - g_variant_unref(result); - } - - if (error) { - if (!(error->domain == G_DBUS_ERROR && - error->code == G_DBUS_ERROR_UNKNOWN_METHOD)) { - std::cerr << "[TrayService] ContextMenu failed for " << id << ": " - << error->message << std::endl; - } - g_error_free(error); - } + G_DBUS_CALL_FLAGS_NONE, kDBusTimeoutMs, nullptr, + &on_simple_call_finished, data); } Glib::RefPtr @@ -485,39 +460,105 @@ TrayService::get_menu_action_group(const std::string &id) { return item.menuActions; } -std::optional -TrayService::get_menu_layout(const std::string &id) { +struct MenuLayoutCallData { + TrayService *self = nullptr; + std::string id; + std::string busName; + std::string menuPath; + TrayService::MenuLayoutCallback callback; +}; + +void on_menu_layout_finished(GObject *source, GAsyncResult *res, + gpointer user_data) { + std::unique_ptr data( + static_cast(user_data)); + if (!data || !data->self) { + return; + } + + GError *error = nullptr; + GVariant *reply = + g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); + + if (error) { + if (data->callback) { + data->callback(std::nullopt); + } + g_error_free(error); + return; + } + + if (!reply) { + if (data->callback) { + data->callback(std::nullopt); + } + return; + } + + GVariant *rootTuple = g_variant_get_child_value(reply, 1); + g_variant_unref(reply); + + if (!rootTuple) { + if (data->callback) { + data->callback(std::nullopt); + } + return; + } + + TrayService::MenuNode rootNode; + parse_menu_node(rootTuple, rootNode); + g_variant_unref(rootTuple); + + if (data->callback) { + data->callback(std::make_optional(std::move(rootNode))); + } +} + +void TrayService::request_menu_layout(const std::string &id, + MenuLayoutCallback callback) { auto it = items.find(id); if (it == items.end() || !connection) { - return std::nullopt; + if (callback) { + callback(std::nullopt); + } + return; } auto &item = *it->second; if (!item.publicData.menuAvailable || item.publicData.menuPath.empty()) { - return std::nullopt; + if (callback) { + callback(std::nullopt); + } + return; } call_about_to_show(connection, item.publicData.busName, item.publicData.menuPath, 0); - GVariant *result = call_get_layout(connection, item.publicData.busName, - item.publicData.menuPath); - if (!result) { - return std::nullopt; + auto data = new MenuLayoutCallData(); + data->self = this; + data->id = id; + data->busName = item.publicData.busName; + data->menuPath = item.publicData.menuPath; + data->callback = std::move(callback); + + GVariant *properties = create_property_list_variant(); + if (!properties) { + if (data->callback) { + data->callback(std::nullopt); + } + delete data; + return; } - GVariant *rootTuple = g_variant_get_child_value(result, 1); - g_variant_unref(result); + // g_variant_new consumes the floating reference for '@as'. + GVariant *params = g_variant_new("(ii@as)", 0, -1, properties); - if (!rootTuple) { - return std::nullopt; - } - - MenuNode rootNode; - parse_menu_node(rootTuple, rootNode); - g_variant_unref(rootTuple); - - return rootNode; + g_dbus_connection_call(connection->gobj(), data->busName.c_str(), + data->menuPath.c_str(), kDBusMenuInterface, + "GetLayout", params, nullptr, + G_DBUS_CALL_FLAGS_NONE, kDBusMenuTimeoutMs, nullptr, + &on_menu_layout_finished, data); } bool TrayService::activate_menu_item(const std::string &id, int itemId) { @@ -531,28 +572,29 @@ bool TrayService::activate_menu_item(const std::string &id, int itemId) { return false; } - GVariant *emptyData = - g_variant_new_array(G_VARIANT_TYPE("{sv}"), nullptr, 0); - GVariant *params = g_variant_new( - "(isvu)", itemId, "clicked", g_variant_new_variant(emptyData), + // Some tray items update state lazily and require AboutToShow(itemId) + // before handling a click. + call_about_to_show(connection, item.publicData.busName, + item.publicData.menuPath, itemId); + + // dbusmenu Event signature: (i s v u) + // For "clicked", the payload is typically an a{sv} dictionary. + // IMPORTANT: the 'v' argument must be a variant container, so we wrap. + GVariantBuilder dict; + g_variant_builder_init(&dict, G_VARIANT_TYPE("a{sv}")); + GVariant *payloadDict = g_variant_builder_end(&dict); + GVariant *payload = g_variant_new_variant(payloadDict); + GVariant *params = g_variant_new( + "(isvu)", itemId, "clicked", payload, static_cast(g_get_monotonic_time() / 1000)); - GError *error = nullptr; - GVariant *result = g_dbus_connection_call_sync( - connection->gobj(), item.publicData.busName.c_str(), - item.publicData.menuPath.c_str(), kDBusMenuInterface, "Event", params, - nullptr, G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); - - if (result) { - g_variant_unref(result); - } - - if (error) { - std::cerr << "[TrayService] Event failed for " << id << " (" << itemId - << "): " << error->message << std::endl; - g_error_free(error); - return false; - } + auto data = new SimpleCallData(); + data->debugLabel = "MenuEvent(" + id + "," + std::to_string(itemId) + ")"; + g_dbus_connection_call(connection->gobj(), item.publicData.busName.c_str(), + item.publicData.menuPath.c_str(), kDBusMenuInterface, + "Event", params, nullptr, G_DBUS_CALL_FLAGS_NONE, + kDBusMenuTimeoutMs, nullptr, + &on_simple_call_finished, data); return true; } @@ -696,8 +738,7 @@ void TrayService::register_item(const Glib::ustring &sender, const std::string id = parsed.busName + parsed.objectPath; auto existing = items.find(id); if (existing != items.end()) { - refresh_item(*existing->second); - itemUpdatedSignal.emit(existing->second->publicData); + schedule_refresh(id); return; } @@ -706,7 +747,7 @@ void TrayService::register_item(const Glib::ustring &sender, item->publicData.busName = parsed.busName; item->publicData.objectPath = parsed.objectPath; - refresh_item(*item); + item->addSignalPending = true; item->signalSubscriptionId = g_dbus_connection_signal_subscribe( connection->gobj(), item->publicData.busName.c_str(), nullptr, nullptr, @@ -726,7 +767,7 @@ void TrayService::register_item(const Glib::ustring &sender, std::make_tuple(Glib::ustring(id))); emit_watcher_signal("StatusNotifierItemRegistered", params); - itemAddedSignal.emit(items.at(id)->publicData); + schedule_refresh(id); } void TrayService::unregister_item(const std::string &id) { @@ -735,6 +776,11 @@ void TrayService::unregister_item(const std::string &id) { return; } + if (it->second->refreshSourceId != 0) { + g_source_remove(it->second->refreshSourceId); + it->second->refreshSourceId = 0; + } + if (connection && it->second->signalSubscriptionId != 0) { g_dbus_connection_signal_unsubscribe(connection->gobj(), it->second->signalSubscriptionId); @@ -755,31 +801,58 @@ void TrayService::unregister_item(const std::string &id) { itemRemovedSignal.emit(id); } -void TrayService::refresh_item(TrackedItem &item) { - if (!connection) { +struct RefreshCallData { + TrayService *self = nullptr; + std::string id; + std::string busName; + std::string objectPath; +}; + +void TrayService::on_refresh_finished_static(GObject *source, GAsyncResult *res, + gpointer user_data) { + std::unique_ptr data( + static_cast(user_data)); + if (!data || !data->self) { return; } + auto it = data->self->items.find(data->id); + if (it == data->self->items.end()) { + return; + } + + auto &tracked = *it->second; + GError *error = nullptr; - GVariant *reply = g_dbus_connection_call_sync( - connection->gobj(), item.publicData.busName.c_str(), - item.publicData.objectPath.c_str(), kDBusPropertiesIface, "GetAll", - g_variant_new("(s)", kItemInterface), G_VARIANT_TYPE("(a{sv})"), - G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error); + GVariant *reply = + g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), res, &error); if (!reply) { if (error) { std::cerr << "[TrayService] Failed to query properties for " - << item.publicData.id << ": " << error->message - << std::endl; + << data->id << ": " << error->message << std::endl; g_error_free(error); } + + tracked.refreshInFlight = false; + if (tracked.addSignalPending) { + tracked.addSignalPending = false; + data->self->itemAddedSignal.emit(tracked.publicData); + } + if (tracked.refreshQueued) { + tracked.refreshQueued = false; + data->self->schedule_refresh(data->id); + } return; } GVariant *dictVariant = g_variant_get_child_value(reply, 0); g_variant_unref(reply); - if (!dictVariant) { + tracked.refreshInFlight = false; + if (tracked.refreshQueued) { + tracked.refreshQueued = false; + data->self->schedule_refresh(data->id); + } return; } @@ -812,13 +885,8 @@ void TrayService::refresh_item(TrackedItem &item) { const gchar *str = g_variant_get_string(value, nullptr); status = str ? str : ""; } else if (std::strcmp(key, "Menu") == 0) { - if (g_variant_is_of_type(value, G_VARIANT_TYPE_OBJECT_PATH)) { - const gchar *str = g_variant_get_string(value, nullptr); - menuPath = str ? str : ""; - } else { - const gchar *str = g_variant_get_string(value, nullptr); - menuPath = str ? str : ""; - } + const gchar *str = g_variant_get_string(value, nullptr); + menuPath = str ? str : ""; } else if (std::strcmp(key, "IconName") == 0) { const gchar *str = g_variant_get_string(value, nullptr); iconName = str ? str : ""; @@ -826,39 +894,127 @@ void TrayService::refresh_item(TrackedItem &item) { const gchar *str = g_variant_get_string(value, nullptr); attentionIconName = str ? str : ""; } else if (std::strcmp(key, "IconPixmap") == 0) { - iconTexture = parse_icon_pixmap(value); + iconTexture = TrayService::parse_icon_pixmap(value); } else if (std::strcmp(key, "AttentionIconPixmap") == 0) { - attentionTexture = parse_icon_pixmap(value); + attentionTexture = TrayService::parse_icon_pixmap(value); } g_variant_unref(value); } g_variant_unref(dictVariant); - const bool menuPathChanged = (item.publicData.menuPath != menuPath); - item.publicData.title = title; - item.publicData.status = status; - item.publicData.menuPath = menuPath; - item.publicData.menuAvailable = !menuPath.empty(); - if (menuPathChanged || !item.publicData.menuAvailable) { - item.menuModel.reset(); - item.menuActions.reset(); + const bool menuPathChanged = (tracked.publicData.menuPath != menuPath); + tracked.publicData.title = title; + tracked.publicData.status = status; + tracked.publicData.menuPath = menuPath; + tracked.publicData.menuAvailable = !menuPath.empty(); + + if (menuPathChanged || !tracked.publicData.menuAvailable) { + tracked.menuModel.reset(); + tracked.menuActions.reset(); } - item.publicData.iconName = + + tracked.publicData.iconName = (status == "NeedsAttention" && !attentionIconName.empty()) ? attentionIconName : iconName; if (status == "NeedsAttention" && attentionTexture) { - item.publicData.iconPaintable = attentionTexture; + tracked.publicData.iconPaintable = attentionTexture; } else { - item.publicData.iconPaintable = iconTexture; + tracked.publicData.iconPaintable = iconTexture; } - if (!item.publicData.iconPaintable && iconTexture) { - item.publicData.iconPaintable = iconTexture; + if (!tracked.publicData.iconPaintable && iconTexture) { + tracked.publicData.iconPaintable = iconTexture; } + + tracked.refreshInFlight = false; + + if (tracked.addSignalPending) { + tracked.addSignalPending = false; + data->self->itemAddedSignal.emit(tracked.publicData); + } else { + data->self->itemUpdatedSignal.emit(tracked.publicData); + } + + if (tracked.refreshQueued) { + tracked.refreshQueued = false; + data->self->schedule_refresh(data->id); + } +} + +struct RefreshTimeoutData { + TrayService *self = nullptr; + std::string id; +}; + +gboolean TrayService::refresh_timeout_cb(gpointer user_data) { + std::unique_ptr data( + static_cast(user_data)); + if (!data || !data->self) { + return G_SOURCE_REMOVE; + } + + auto it = data->self->items.find(data->id); + if (it == data->self->items.end()) { + return G_SOURCE_REMOVE; + } + + it->second->refreshSourceId = 0; + data->self->begin_refresh(data->id); + return G_SOURCE_REMOVE; +} + +void TrayService::schedule_refresh(const std::string &id) { + auto it = items.find(id); + if (it == items.end()) { + return; + } + + auto &tracked = *it->second; + if (tracked.refreshSourceId != 0) { + return; + } + + auto *data = new RefreshTimeoutData(); + data->self = this; + data->id = id; + tracked.refreshSourceId = + g_timeout_add(kRefreshDebounceMs, &TrayService::refresh_timeout_cb, data); +} + +void TrayService::begin_refresh(const std::string &id) { + if (!connection) { + return; + } + + auto it = items.find(id); + if (it == items.end()) { + return; + } + + auto &tracked = *it->second; + if (tracked.refreshInFlight) { + tracked.refreshQueued = true; + return; + } + + tracked.refreshInFlight = true; + + auto data = new RefreshCallData(); + data->self = this; + data->id = id; + data->busName = tracked.publicData.busName; + data->objectPath = tracked.publicData.objectPath; + + g_dbus_connection_call(connection->gobj(), data->busName.c_str(), + data->objectPath.c_str(), kDBusPropertiesIface, + "GetAll", g_variant_new("(s)", kItemInterface), + G_VARIANT_TYPE("(a{sv})"), G_DBUS_CALL_FLAGS_NONE, + kDBusTimeoutMs, nullptr, + &TrayService::on_refresh_finished_static, data); } void TrayService::emit_registered_items_changed() { @@ -956,12 +1112,10 @@ void TrayService::on_dbus_signal(const gchar *sender_name, std::strcmp(signal_name, "NewAttentionIcon") == 0 || std::strcmp(signal_name, "NewToolTip") == 0 || std::strcmp(signal_name, "NewMenu") == 0) { - refresh_item(*it->second); - itemUpdatedSignal.emit(it->second->publicData); + schedule_refresh(it->first); } } else if (isPropertiesSignal) { - refresh_item(*it->second); - itemUpdatedSignal.emit(it->second->publicData); + schedule_refresh(it->first); } } diff --git a/src/widgets/tray.cpp b/src/widgets/tray.cpp index 74ae071..ff85457 100644 --- a/src/widgets/tray.cpp +++ b/src/widgets/tray.cpp @@ -3,10 +3,145 @@ #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; +} +} // 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()); + menuPopover->insert_action_group( + "dbusmenu", Glib::RefPtr()); 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(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); + } + service.contextMenu(id, sendX, sendY); } -bool TrayIconWidget::ensure_menu() { - auto layoutOpt = service.get_menu_layout(id); +void TrayIconWidget::on_menu_layout_ready( + std::optional 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(std::lround(pendingX)), + static_cast(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(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(pendingX), static_cast(pendingY), 1, 1); menuPopover->set_pointing_to(rect);