1250 lines
39 KiB
C++
1250 lines
39 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 <spdlog/spdlog.h>
|
|
#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)) {
|
|
spdlog::error("[TrayService] {} failed: {}",
|
|
(data ? data->debugLabel : std::string("D-Bus call")),
|
|
error->message);
|
|
}
|
|
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) {
|
|
spdlog::error(
|
|
"[TrayService] Failed to parse introspection data: {}",
|
|
err.what());
|
|
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;
|
|
|
|
spdlog::debug(
|
|
"[TrayService] MenuEvent id={} item={} x={} y={} button={} tsMs={}",
|
|
id, itemId, x, y, button, ts);
|
|
|
|
// 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) {
|
|
spdlog::error("[TrayService] Missing interface info for watcher.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
registrationId = connection->register_object(kWatcherObjectPath,
|
|
interface_info, vtable);
|
|
} catch (const Glib::Error &err) {
|
|
spdlog::error(
|
|
"[TrayService] Failed to register watcher object: {}",
|
|
err.what());
|
|
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 ¶meters,
|
|
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()) {
|
|
spdlog::warn("[TrayService] Invalid service registration: {}",
|
|
service);
|
|
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) {
|
|
spdlog::error(
|
|
"[TrayService] Failed to query properties for {}: {}",
|
|
data->id, error->message);
|
|
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 ¶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<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) {
|
|
spdlog::error("[TrayService] Failed to create texture: {}",
|
|
err.what());
|
|
return {};
|
|
}
|
|
}
|