#include "services/tray.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr const char *kWatcherBusName = "org.kde.StatusNotifierWatcher"; constexpr const char *kWatcherObjectPath = "/StatusNotifierWatcher"; constexpr const char *kWatcherInterface = "org.kde.StatusNotifierWatcher"; 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"( )"; struct ParsedService { std::string busName; std::string objectPath; }; ParsedService parse_service_identifier(const Glib::ustring &sender, const std::string &service) { ParsedService parsed; if (service.empty()) { parsed.busName = sender; parsed.objectPath = "/StatusNotifierItem"; return parsed; } if (service.front() == '/') { parsed.busName = sender; parsed.objectPath = service; return parsed; } const auto slash = service.find('/'); if (slash == std::string::npos) { parsed.busName = service; parsed.objectPath = "/StatusNotifierItem"; return parsed; } parsed.busName = service.substr(0, slash); parsed.objectPath = service.substr(slash); if (parsed.busName.empty()) { parsed.busName = sender; } if (parsed.objectPath.empty()) { parsed.objectPath = "/StatusNotifierItem"; } return parsed; } GVariant *create_property_list_variant() { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); const char *properties[] = {"label", "label-markup", "enabled", "visible", "children-display", "type", "toggle-type", "toggle-state"}; for (const char *prop : properties) { g_variant_builder_add(&builder, "s", prop); } return g_variant_builder_end(&builder); } void call_about_to_show(const Glib::RefPtr &connection, const std::string &busName, const std::string &menuPath, int id) { if (!connection) { return; } 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); } 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); } if (!error) { return; } 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; } g_error_free(error); } void parse_menu_node(GVariant *tuple, TrayService::MenuNode &outNode) { if (!tuple) { return; } if (g_variant_is_of_type(tuple, G_VARIANT_TYPE_VARIANT)) { GVariant *inner = g_variant_get_variant(tuple); if (!inner) { return; } parse_menu_node(inner, outNode); g_variant_unref(inner); return; } if (!g_variant_is_of_type(tuple, G_VARIANT_TYPE_TUPLE)) { return; } int id = 0; GVariant *propsVariant = nullptr; GVariant *childrenVariant = nullptr; g_variant_get(tuple, "(i@a{sv}@av)", &id, &propsVariant, &childrenVariant); outNode.id = id; outNode.enabled = true; outNode.visible = true; outNode.separator = false; outNode.label.clear(); if (propsVariant) { GVariantIter iter; g_variant_iter_init(&iter, propsVariant); const gchar *key = nullptr; GVariant *value = nullptr; while (g_variant_iter_next(&iter, "{sv}", &key, &value)) { if (!key || !value) { if (value) { g_variant_unref(value); } continue; } GVariant *unboxed = value; bool unboxedOwned = false; if (g_variant_is_of_type(unboxed, G_VARIANT_TYPE_VARIANT)) { unboxed = g_variant_get_variant(unboxed); if (!unboxed) { g_variant_unref(value); continue; } unboxedOwned = true; } if (std::strcmp(key, "label") == 0) { if (g_variant_is_of_type(unboxed, G_VARIANT_TYPE_STRING)) { const gchar *str = g_variant_get_string(unboxed, nullptr); outNode.label = str ? str : ""; } } else if (std::strcmp(key, "label-markup") == 0 && outNode.label.empty()) { if (g_variant_is_of_type(unboxed, G_VARIANT_TYPE_STRING)) { const gchar *str = g_variant_get_string(unboxed, nullptr); outNode.label = str ? str : ""; } } else if (std::strcmp(key, "enabled") == 0) { if (g_variant_is_of_type(unboxed, G_VARIANT_TYPE_BOOLEAN)) { outNode.enabled = g_variant_get_boolean(unboxed); } } else if (std::strcmp(key, "visible") == 0) { if (g_variant_is_of_type(unboxed, G_VARIANT_TYPE_BOOLEAN)) { outNode.visible = g_variant_get_boolean(unboxed); } } else if (std::strcmp(key, "type") == 0) { if (g_variant_is_of_type(unboxed, G_VARIANT_TYPE_STRING)) { const gchar *str = g_variant_get_string(unboxed, nullptr); if (str && std::strcmp(str, "separator") == 0) { outNode.separator = true; } } } if (unboxedOwned) { g_variant_unref(unboxed); } g_variant_unref(value); } g_variant_unref(propsVariant); } if (childrenVariant) { gsize count = g_variant_n_children(childrenVariant); outNode.children.reserve(count); for (gsize i = 0; i < count; ++i) { GVariant *childTuple = g_variant_get_child_value(childrenVariant, i); TrayService::MenuNode childNode; parse_menu_node(childTuple, childNode); g_variant_unref(childTuple); if (childNode.visible) { outNode.children.push_back(std::move(childNode)); } } g_variant_unref(childrenVariant); } } } // namespace TrayService::TrayService() : vtable(sigc::mem_fun(*this, &TrayService::handle_method_call), sigc::mem_fun(*this, &TrayService::handle_get_property_slot), sigc::mem_fun(*this, &TrayService::handle_set_property_slot)) {} TrayService::~TrayService() { stop(); } void TrayService::start() { if (nameOwnerId != 0) { return; } try { Gio::init(); } catch (const Glib::Error &) { // Already initialised; ignore. } if (!nodeInfo) { try { nodeInfo = Gio::DBus::NodeInfo::create_for_xml(kWatcherIntrospection); } catch (const Glib::Error &err) { std::cerr << "[TrayService] Failed to parse introspection data: " << err.what() << std::endl; return; } } nameOwnerId = Gio::DBus::own_name( Gio::DBus::BusType::SESSION, kWatcherBusName, sigc::mem_fun(*this, &TrayService::on_bus_acquired), sigc::mem_fun(*this, &TrayService::on_name_acquired), sigc::mem_fun(*this, &TrayService::on_name_lost)); } 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); } if (pair.second->ownerWatchId != 0) { g_bus_unwatch_name(pair.second->ownerWatchId); } } } items.clear(); if (connection && registrationId != 0) { connection->unregister_object(registrationId); registrationId = 0; } if (nameOwnerId != 0) { Gio::DBus::unown_name(nameOwnerId); nameOwnerId = 0; } connection.reset(); hostRegistered = false; } std::vector TrayService::snapshotItems() const { std::vector result; result.reserve(items.size()); for (const auto &pair : items) { result.push_back(pair.second->publicData); } return result; } const TrayService::Item *TrayService::findItem(const std::string &id) const { auto it = items.find(id); if (it == items.end()) { return nullptr; } return &it->second->publicData; } void TrayService::activate(const std::string &id, int32_t x, int32_t y) { auto it = items.find(id); if (it == items.end() || !connection) { return; } 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, kDBusTimeoutMs, nullptr, &on_simple_call_finished, data); } void TrayService::secondaryActivate(const std::string &id, int32_t x, int32_t y) { auto it = items.find(id); if (it == items.end() || !connection) { return; } 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, kDBusTimeoutMs, nullptr, &on_simple_call_finished, data); } void TrayService::contextMenu(const std::string &id, int32_t x, int32_t y) { auto it = items.find(id); if (it == items.end() || !connection) { return; } 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, kDBusTimeoutMs, nullptr, &on_simple_call_finished, data); } Glib::RefPtr TrayService::get_menu_model(const std::string &id) { auto it = items.find(id); if (it == items.end() || !connection) { return {}; } auto &item = *it->second; if (!item.publicData.menuAvailable || item.publicData.menuPath.empty()) { item.menuModel.reset(); item.menuActions.reset(); return {}; } if (!item.menuModel) { GDBusMenuModel *dbusModel = g_dbus_menu_model_get( connection->gobj(), item.publicData.busName.c_str(), item.publicData.menuPath.c_str()); if (!dbusModel) { return {}; } item.menuModel = Glib::wrap(G_MENU_MODEL(dbusModel), false); } return item.menuModel; } Glib::RefPtr TrayService::get_menu_action_group(const std::string &id) { auto it = items.find(id); if (it == items.end() || !connection) { return {}; } auto &item = *it->second; if (!item.publicData.menuAvailable || item.publicData.menuPath.empty()) { item.menuActions.reset(); return {}; } if (!item.menuActions) { auto action_group = Gio::DBus::ActionGroup::get( connection, item.publicData.busName, item.publicData.menuPath); if (!action_group) { return {}; } item.menuActions = action_group; } return item.menuActions; } 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) { if (callback) { callback(std::nullopt); } return; } auto &item = *it->second; if (!item.publicData.menuAvailable || item.publicData.menuPath.empty()) { if (callback) { callback(std::nullopt); } return; } call_about_to_show(connection, item.publicData.busName, item.publicData.menuPath, 0); 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; } // g_variant_new consumes the floating reference for '@as'. GVariant *params = g_variant_new("(ii@as)", 0, -1, properties); 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) { auto it = items.find(id); if (it == items.end() || !connection) { return false; } auto &item = *it->second; if (!item.publicData.menuAvailable || item.publicData.menuPath.empty()) { return false; } // 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)); 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; } sigc::signal & TrayService::signal_item_added() { return itemAddedSignal; } sigc::signal &TrayService::signal_item_removed() { return itemRemovedSignal; } sigc::signal & TrayService::signal_item_updated() { return itemUpdatedSignal; } void TrayService::on_bus_acquired( const Glib::RefPtr &connection, const Glib::ustring &) { this->connection = connection; auto interface_info = nodeInfo->lookup_interface(kWatcherInterface); if (!interface_info) { std::cerr << "[TrayService] Missing interface info for watcher." << std::endl; return; } try { registrationId = connection->register_object(kWatcherObjectPath, interface_info, vtable); } catch (const Glib::Error &err) { std::cerr << "[TrayService] Failed to register watcher object: " << err.what() << std::endl; registrationId = 0; return; } hostRegistered = true; emit_watcher_signal("StatusNotifierHostRegistered", Glib::VariantContainerBase()); } void TrayService::on_name_acquired( const Glib::RefPtr & /*connection*/, const Glib::ustring &) {} void TrayService::on_name_lost( const Glib::RefPtr & /*connection*/, const Glib::ustring &) { stop(); } void TrayService::handle_method_call( const Glib::RefPtr & /*connection*/, const Glib::ustring &sender, const Glib::ustring & /*object_path*/, const Glib::ustring & /*interface_name*/, const Glib::ustring &method_name, const Glib::VariantContainerBase ¶meters, const Glib::RefPtr &invocation) { if (method_name == "RegisterStatusNotifierItem") { Glib::ustring service; if (auto rawParams = parameters.gobj()) { GVariant *child = g_variant_get_child_value(const_cast(rawParams), 0); if (child) { service = Glib::ustring(g_variant_get_string(child, nullptr)); g_variant_unref(child); } } register_item(sender, service); invocation->return_value(Glib::VariantContainerBase()); return; } if (method_name == "RegisterStatusNotifierHost") { hostRegistered = true; emit_watcher_signal("StatusNotifierHostRegistered", Glib::VariantContainerBase()); invocation->return_value(Glib::VariantContainerBase()); return; } invocation->return_dbus_error("org.freedesktop.DBus.Error.UnknownMethod", "Unknown method: " + method_name); } void TrayService::handle_get_property_slot( Glib::VariantBase &result, const Glib::RefPtr & /*connection*/, const Glib::ustring & /*sender*/, const Glib::ustring & /*object_path*/, const Glib::ustring & /*interface_name*/, const Glib::ustring &property_name) { result = handle_get_property(property_name); } bool TrayService::handle_set_property_slot( const Glib::RefPtr & /*connection*/, const Glib::ustring & /*sender*/, const Glib::ustring & /*object_path*/, const Glib::ustring & /*interface_name*/, const Glib::ustring &property_name, const Glib::VariantBase &value) { return handle_set_property(property_name, value); } Glib::VariantBase TrayService::handle_get_property(const Glib::ustring &property_name) { if (property_name == "RegisteredStatusNotifierItems") { return create_registered_items_variant(); } if (property_name == "IsStatusNotifierHostRegistered") { return Glib::Variant::create(hostRegistered); } if (property_name == "ProtocolVersion") { return Glib::Variant::create(0); } return Glib::VariantBase(); } bool TrayService::handle_set_property(const Glib::ustring &, const Glib::VariantBase &) { // Read-only properties; ignore setters. return false; } void TrayService::register_item(const Glib::ustring &sender, const std::string &service) { if (!connection) { return; } ParsedService parsed = parse_service_identifier(sender, service); if (parsed.busName.empty() || parsed.objectPath.empty()) { std::cerr << "[TrayService] Invalid service registration: " << service << std::endl; return; } const std::string id = parsed.busName + parsed.objectPath; auto existing = items.find(id); if (existing != items.end()) { schedule_refresh(id); return; } auto item = std::make_unique(); item->publicData.id = id; item->publicData.busName = parsed.busName; item->publicData.objectPath = parsed.objectPath; item->addSignalPending = true; item->signalSubscriptionId = g_dbus_connection_signal_subscribe( connection->gobj(), item->publicData.busName.c_str(), nullptr, nullptr, item->publicData.objectPath.c_str(), nullptr, G_DBUS_SIGNAL_FLAGS_NONE, &TrayService::on_dbus_signal_static, this, nullptr); item->ownerWatchId = g_bus_watch_name_on_connection( connection->gobj(), item->publicData.busName.c_str(), G_BUS_NAME_WATCHER_FLAGS_NONE, nullptr, &TrayService::on_name_vanished_static, this, nullptr); items.emplace(id, std::move(item)); emit_registered_items_changed(); auto params = Glib::Variant>::create( std::make_tuple(Glib::ustring(id))); emit_watcher_signal("StatusNotifierItemRegistered", params); schedule_refresh(id); } void TrayService::unregister_item(const std::string &id) { auto it = items.find(id); if (it == items.end()) { 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); } if (it->second->ownerWatchId != 0) { g_bus_unwatch_name(it->second->ownerWatchId); } items.erase(it); emit_registered_items_changed(); auto params = Glib::Variant>::create( std::make_tuple(Glib::ustring(id))); emit_watcher_signal("StatusNotifierItemUnregistered", params); itemRemovedSignal.emit(id); } 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_finish(G_DBUS_CONNECTION(source), res, &error); if (!reply) { if (error) { std::cerr << "[TrayService] Failed to query properties for " << 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; } GVariantIter iter; g_variant_iter_init(&iter, dictVariant); const gchar *key = nullptr; GVariant *value = nullptr; Glib::RefPtr iconTexture; Glib::RefPtr attentionTexture; std::string iconName; std::string attentionIconName; std::string title; std::string status; std::string menuPath; while (g_variant_iter_next(&iter, "{&sv}", &key, &value)) { if (!key || !value) { if (value) { g_variant_unref(value); } continue; } if (std::strcmp(key, "Title") == 0) { const gchar *str = g_variant_get_string(value, nullptr); title = str ? str : ""; } else if (std::strcmp(key, "Status") == 0) { const gchar *str = g_variant_get_string(value, nullptr); status = str ? str : ""; } else if (std::strcmp(key, "Menu") == 0) { 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 : ""; } else if (std::strcmp(key, "AttentionIconName") == 0) { const gchar *str = g_variant_get_string(value, nullptr); attentionIconName = str ? str : ""; } else if (std::strcmp(key, "IconPixmap") == 0) { iconTexture = TrayService::parse_icon_pixmap(value); } else if (std::strcmp(key, "AttentionIconPixmap") == 0) { attentionTexture = TrayService::parse_icon_pixmap(value); } g_variant_unref(value); } g_variant_unref(dictVariant); 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(); } tracked.publicData.iconName = (status == "NeedsAttention" && !attentionIconName.empty()) ? attentionIconName : iconName; if (status == "NeedsAttention" && attentionTexture) { tracked.publicData.iconPaintable = attentionTexture; } else { tracked.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() { if (!connection) { return; } GVariantBuilder changed_builder; g_variant_builder_init(&changed_builder, G_VARIANT_TYPE("a{sv}")); GVariant *items_array = create_registered_items_variant().gobj_copy(); if (!items_array) { items_array = g_variant_new_strv(nullptr, 0); } GVariant *items_variant = g_variant_new_variant(items_array); g_variant_builder_add(&changed_builder, "{sv}", "RegisteredStatusNotifierItems", items_variant); g_variant_unref(items_array); GVariantBuilder invalidated_builder; g_variant_builder_init(&invalidated_builder, G_VARIANT_TYPE("as")); GVariant *params = g_variant_new("(sa{sv}as)", kWatcherInterface, &changed_builder, &invalidated_builder); g_dbus_connection_emit_signal(connection->gobj(), nullptr, kWatcherObjectPath, kDBusPropertiesIface, "PropertiesChanged", params, nullptr); } Glib::Variant> TrayService::create_registered_items_variant() const { std::vector values; values.reserve(items.size()); for (const auto &pair : items) { values.emplace_back(pair.second->publicData.id); } return Glib::Variant>::create(values); } void TrayService::emit_watcher_signal( const Glib::ustring &signal_name, const Glib::VariantContainerBase ¶meters) { if (!connection) { return; } GVariant *payload = parameters.gobj() ? parameters.gobj_copy() : nullptr; g_dbus_connection_emit_signal(connection->gobj(), nullptr, kWatcherObjectPath, kWatcherInterface, signal_name.c_str(), payload, nullptr); } void TrayService::on_dbus_signal_static( GDBusConnection * /*connection*/, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { if (auto *self = static_cast(user_data)) { self->on_dbus_signal(sender_name, object_path, interface_name, signal_name, parameters); } } void TrayService::on_dbus_signal(const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant * /*parameters*/) { if (!sender_name || !object_path || !signal_name || !interface_name) { return; } auto it = std::find_if(items.begin(), items.end(), [&](const auto &pair) { return pair.second->publicData.busName == sender_name && pair.second->publicData.objectPath == object_path; }); if (it == items.end()) { return; } const bool isNotifierSignal = std::strcmp(interface_name, kItemInterface) == 0; const bool isPropertiesSignal = std::strcmp(interface_name, kDBusPropertiesIface) == 0 && std::strcmp(signal_name, "PropertiesChanged") == 0; if (isNotifierSignal) { if (std::strcmp(signal_name, "NewIcon") == 0 || std::strcmp(signal_name, "NewStatus") == 0 || std::strcmp(signal_name, "NewTitle") == 0 || std::strcmp(signal_name, "NewAttentionIcon") == 0 || std::strcmp(signal_name, "NewToolTip") == 0 || std::strcmp(signal_name, "NewMenu") == 0) { schedule_refresh(it->first); } } else if (isPropertiesSignal) { schedule_refresh(it->first); } } void TrayService::on_name_vanished_static(GDBusConnection * /*connection*/, const gchar *name, gpointer user_data) { if (auto *self = static_cast(user_data)) { self->on_name_vanished(name); } } void TrayService::on_name_vanished(const gchar *bus_name) { if (!bus_name) { return; } std::vector toRemove; for (const auto &pair : items) { if (pair.second->publicData.busName == bus_name) { toRemove.push_back(pair.first); } } for (const auto &id : toRemove) { unregister_item(id); } } Glib::RefPtr TrayService::parse_icon_pixmap(GVariant *variant) { if (!variant || g_variant_n_children(variant) == 0) { return {}; } GVariant *entry = g_variant_get_child_value(variant, 0); if (!entry) { return {}; } int32_t width = 0; int32_t height = 0; g_variant_get_child(entry, 0, "i", &width); g_variant_get_child(entry, 1, "i", &height); if (width <= 0 || height <= 0) { g_variant_unref(entry); return {}; } GVariant *bytesVariant = g_variant_get_child_value(entry, 2); if (!bytesVariant) { g_variant_unref(entry); return {}; } gsize rawLength = 0; const guint8 *rawBytes = static_cast( g_variant_get_fixed_array(bytesVariant, &rawLength, sizeof(guint8))); if (!rawBytes || rawLength < static_cast(width * height * 4)) { g_variant_unref(bytesVariant); g_variant_unref(entry); return {}; } std::vector rgba; const std::size_t pixelCount = static_cast(width) * static_cast(height); rgba.resize(pixelCount * 4); const guint32 *pixels = reinterpret_cast(rawBytes); for (std::size_t idx = 0; idx < pixelCount; ++idx) { const guint32 pixel = pixels[idx]; const guint8 a = static_cast((pixel >> 24) & 0xFF); const guint8 r = static_cast((pixel >> 16) & 0xFF); const guint8 g = static_cast((pixel >> 8) & 0xFF); const guint8 b = static_cast(pixel & 0xFF); const std::size_t base = idx * 4; rgba[base + 0] = r; rgba[base + 1] = g; rgba[base + 2] = b; rgba[base + 3] = a; } auto pixbuf = Gdk::Pixbuf::create(Gdk::Colorspace::RGB, true, 8, width, height); if (!pixbuf) { g_variant_unref(bytesVariant); g_variant_unref(entry); return {}; } auto *dest = pixbuf->get_pixels(); const int destRowstride = pixbuf->get_rowstride(); const int srcRowstride = width * 4; for (int y = 0; y < height; ++y) { std::memcpy(dest + y * destRowstride, rgba.data() + static_cast(y) * srcRowstride, static_cast(srcRowstride)); } g_variant_unref(bytesVariant); g_variant_unref(entry); try { return Gdk::Texture::create_for_pixbuf(pixbuf); } catch (const Glib::Error &err) { std::cerr << "[TrayService] Failed to create texture: " << err.what() << std::endl; return {}; } }