Files
bar/src/services/tray.cpp
2026-01-03 22:55:02 +01:00

1248 lines
40 KiB
C++

#include "services/tray.hpp"
#include <algorithm>
#include <cstring>
#include <gdkmm/pixbuf.h>
#include <gdkmm/texture.h>
#include <gio/gdbusmenumodel.h>
#include <gio/gio.h>
#include <giomm/dbusactiongroup.h>
#include <giomm/dbusownname.h>
#include <giomm/menumodel.h>
#include <iostream>
#include <memory>
#include <tuple>
#include <vector>
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"(<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-Bus Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.StatusNotifierWatcher">
<method name="RegisterStatusNotifierItem">
<arg type="s" name="service" direction="in"/>
</method>
<method name="RegisterStatusNotifierHost">
<arg type="s" name="service" direction="in"/>
</method>
<property name="RegisteredStatusNotifierItems" type="as" access="read"/>
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
<property name="ProtocolVersion" type="i" access="read"/>
<signal name="StatusNotifierItemRegistered">
<arg type="s" name="service"/>
</signal>
<signal name="StatusNotifierItemUnregistered">
<arg type="s" name="service"/>
</signal>
<signal name="StatusNotifierHostRegistered"/>
</interface>
</node>)";
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<Gio::DBus::Connection> &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<SimpleCallData> data(
static_cast<SimpleCallData *>(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::Item> TrayService::snapshotItems() const {
std::vector<Item> 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<Gio::MenuModel>
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<Gio::ActionGroup>
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<MenuLayoutCallData> data(
static_cast<MenuLayoutCallData *>(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,
int32_t x, int32_t y, uint32_t button,
uint32_t timestampMs) {
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;
}
const guint32 nowMs = static_cast<guint32>(g_get_real_time() / 1000);
const guint32 ts = timestampMs ? timestampMs : nowMs;
std::cerr << "[TrayService] MenuEvent id=" << id << " item=" << itemId
<< " x=" << x << " y=" << y << " button=" << button
<< " tsMs=" << ts << std::endl;
// dbusmenu Event signature: (i s v u)
// Some handlers (e.g., media players) look for both "timestamp" and
// "time" keys; send both alongside coords/button when available.
GVariantBuilder dict;
g_variant_builder_init(&dict, G_VARIANT_TYPE("a{sv}"));
g_variant_builder_add(&dict, "{sv}", "timestamp",
g_variant_new_uint32(ts));
g_variant_builder_add(&dict, "{sv}", "time", g_variant_new_uint32(ts));
if (x != -1 && y != -1) {
g_variant_builder_add(&dict, "{sv}", "x", g_variant_new_int32(x));
g_variant_builder_add(&dict, "{sv}", "y", g_variant_new_int32(y));
}
if (button > 0) {
g_variant_builder_add(
&dict, "{sv}", "button",
g_variant_new_int32(static_cast<int32_t>(button)));
}
GVariant *payloadDict = g_variant_builder_end(&dict);
GVariant *payload = g_variant_new_variant(payloadDict);
GVariant *params = g_variant_new("(isvu)", itemId, "clicked",
payload, ts);
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<void(const TrayService::Item &)> &
TrayService::signal_item_added() {
return itemAddedSignal;
}
sigc::signal<void(const std::string &)> &TrayService::signal_item_removed() {
return itemRemovedSignal;
}
sigc::signal<void(const TrayService::Item &)> &
TrayService::signal_item_updated() {
return itemUpdatedSignal;
}
void TrayService::on_bus_acquired(
const Glib::RefPtr<Gio::DBus::Connection> &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<Gio::DBus::Connection> & /*connection*/,
const Glib::ustring &) {}
void TrayService::on_name_lost(
const Glib::RefPtr<Gio::DBus::Connection> & /*connection*/,
const Glib::ustring &) {
stop();
}
void TrayService::handle_method_call(
const Glib::RefPtr<Gio::DBus::Connection> & /*connection*/,
const Glib::ustring &sender, const Glib::ustring & /*object_path*/,
const Glib::ustring & /*interface_name*/, const Glib::ustring &method_name,
const Glib::VariantContainerBase &parameters,
const Glib::RefPtr<Gio::DBus::MethodInvocation> &invocation) {
if (method_name == "RegisterStatusNotifierItem") {
Glib::ustring service;
if (auto rawParams = parameters.gobj()) {
GVariant *child =
g_variant_get_child_value(const_cast<GVariant *>(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<Gio::DBus::Connection> & /*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<Gio::DBus::Connection> & /*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<bool>::create(hostRegistered);
}
if (property_name == "ProtocolVersion") {
return Glib::Variant<int32_t>::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<TrackedItem>();
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<std::tuple<Glib::ustring>>::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<std::tuple<Glib::ustring>>::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<RefreshCallData> data(
static_cast<RefreshCallData *>(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<Gdk::Paintable> iconTexture;
Glib::RefPtr<Gdk::Paintable> 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<RefreshTimeoutData> data(
static_cast<RefreshTimeoutData *>(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<std::vector<Glib::ustring>>
TrayService::create_registered_items_variant() const {
std::vector<Glib::ustring> values;
values.reserve(items.size());
for (const auto &pair : items) {
values.emplace_back(pair.second->publicData.id);
}
return Glib::Variant<std::vector<Glib::ustring>>::create(values);
}
void TrayService::emit_watcher_signal(
const Glib::ustring &signal_name,
const Glib::VariantContainerBase &parameters) {
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<TrayService *>(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<TrayService *>(user_data)) {
self->on_name_vanished(name);
}
}
void TrayService::on_name_vanished(const gchar *bus_name) {
if (!bus_name) {
return;
}
std::vector<std::string> 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<Gdk::Paintable> 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<const guint8 *>(
g_variant_get_fixed_array(bytesVariant, &rawLength, sizeof(guint8)));
if (!rawBytes || rawLength < static_cast<gsize>(width * height * 4)) {
g_variant_unref(bytesVariant);
g_variant_unref(entry);
return {};
}
std::vector<guint8> rgba;
const std::size_t pixelCount =
static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
rgba.resize(pixelCount * 4);
const guint32 *pixels = reinterpret_cast<const guint32 *>(rawBytes);
for (std::size_t idx = 0; idx < pixelCount; ++idx) {
const guint32 pixel = pixels[idx];
const guint8 a = static_cast<guint8>((pixel >> 24) & 0xFF);
const guint8 r = static_cast<guint8>((pixel >> 16) & 0xFF);
const guint8 g = static_cast<guint8>((pixel >> 8) & 0xFF);
const guint8 b = static_cast<guint8>(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<std::size_t>(y) * srcRowstride,
static_cast<std::size_t>(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 {};
}
}