add tray icons

This commit is contained in:
2025-12-10 01:35:39 +01:00
parent 7bd4c72763
commit 70a271fb8b
6 changed files with 677 additions and 8 deletions

View File

@@ -1,8 +1,11 @@
#include "services/tray.hpp"
#include <giomm/dbusactiongroup.h>
#include <giomm/dbusownname.h>
#include <giomm/menumodel.h>
#include <gdkmm/pixbuf.h>
#include <gdkmm/texture.h>
#include <gio/gdbusmenumodel.h>
#include <algorithm>
#include <cstring>
@@ -17,6 +20,7 @@ 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";
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>
@@ -86,6 +90,243 @@ ParsedService parse_service_identifier(const Glib::ustring &sender, const std::s
}
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;
}
GError *error = nullptr;
GVariant *result = g_dbus_connection_call_sync(connection->gobj(),
busName.c_str(),
menuPath.c_str(),
kDBusMenuInterface,
"AboutToShow",
g_variant_new("(i)", id),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
&error);
if (result)
{
g_variant_unref(result);
}
if (error)
{
std::cerr << "[TrayService] AboutToShow failed for " << busName << menuPath << " (" << id << "): " << error->message << std::endl;
g_error_free(error);
}
}
GVariant *call_get_layout(const Glib::RefPtr<Gio::DBus::Connection> &connection,
const std::string &busName,
const std::string &menuPath)
{
if (!connection)
{
return nullptr;
}
GVariant *properties = create_property_list_variant();
if (!properties)
{
return nullptr;
}
GVariant *params = g_variant_new("(ii@as)", 0, -1, properties);
g_variant_ref_sink(properties);
GError *error = nullptr;
GVariant *result = g_dbus_connection_call_sync(connection->gobj(),
busName.c_str(),
menuPath.c_str(),
kDBusMenuInterface,
"GetLayout",
params,
nullptr,
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
&error);
g_variant_unref(properties);
if (error)
{
std::cerr << "[TrayService] GetLayout failed for " << busName << menuPath << ": " << error->message << std::endl;
g_error_free(error);
return nullptr;
}
return result;
}
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
@@ -297,6 +538,186 @@ void TrayService::contextMenu(const std::string &id, int32_t x, int32_t y)
}
}
Glib::RefPtr<Gio::MenuModel> TrayService::get_menu_model(const std::string &id)
{
auto it = m_items.find(id);
if (it == m_items.end() || !m_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(m_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 = m_items.find(id);
if (it == m_items.end() || !m_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(m_connection,
item.publicData.busName,
item.publicData.menuPath);
if (!action_group)
{
return {};
}
item.menuActions = action_group;
}
return item.menuActions;
}
void TrayService::debug_dump_menu_layout(const std::string &id)
{
auto it = m_items.find(id);
if (it == m_items.end() || !m_connection)
{
return;
}
const auto &item = *it->second;
if (!item.publicData.menuAvailable || item.publicData.menuPath.empty())
{
return;
}
GVariant *result = call_get_layout(m_connection, item.publicData.busName, item.publicData.menuPath);
if (!result)
{
return;
}
gchar *printed = g_variant_print(result, TRUE);
if (printed)
{
std::cout << "[TrayService] GetLayout for " << id << ":\n" << printed << std::endl;
g_free(printed);
}
g_variant_unref(result);
}
std::optional<TrayService::MenuNode> TrayService::get_menu_layout(const std::string &id)
{
auto it = m_items.find(id);
if (it == m_items.end() || !m_connection)
{
return std::nullopt;
}
auto &item = *it->second;
if (!item.publicData.menuAvailable || item.publicData.menuPath.empty())
{
return std::nullopt;
}
call_about_to_show(m_connection, item.publicData.busName, item.publicData.menuPath, 0);
GVariant *result = call_get_layout(m_connection, item.publicData.busName, item.publicData.menuPath);
if (!result)
{
return std::nullopt;
}
GVariant *rootTuple = g_variant_get_child_value(result, 1);
g_variant_unref(result);
if (!rootTuple)
{
return std::nullopt;
}
MenuNode rootNode;
parse_menu_node(rootTuple, rootNode);
g_variant_unref(rootTuple);
return rootNode;
}
bool TrayService::activate_menu_item(const std::string &id, int itemId)
{
auto it = m_items.find(id);
if (it == m_items.end() || !m_connection)
{
return false;
}
auto &item = *it->second;
if (!item.publicData.menuAvailable || item.publicData.menuPath.empty())
{
return false;
}
GVariant *emptyData = g_variant_new_array(G_VARIANT_TYPE("{sv}"), nullptr, 0);
GVariant *params = g_variant_new("(isvu)",
itemId,
"clicked",
g_variant_new_variant(emptyData),
static_cast<guint32>(g_get_monotonic_time() / 1000));
GError *error = nullptr;
GVariant *result = g_dbus_connection_call_sync(m_connection->gobj(),
item.publicData.busName.c_str(),
item.publicData.menuPath.c_str(),
kDBusMenuInterface,
"Event",
params,
nullptr,
G_DBUS_CALL_FLAGS_NONE,
-1,
nullptr,
&error);
if (result)
{
g_variant_unref(result);
}
if (error)
{
std::cerr << "[TrayService] Event failed for " << id << " (" << itemId << "): " << error->message << std::endl;
g_error_free(error);
return false;
}
return true;
}
sigc::signal<void(const TrayService::Item &)> &TrayService::signal_item_added()
{
return m_itemAddedSignal;
@@ -624,11 +1045,17 @@ void TrayService::refresh_item(TrackedItem &item)
}
g_variant_unref(dictVariant);
const bool menuPathChanged = (item.publicData.menuPath != menuPath);
item.publicData.title = title;
item.publicData.status = status;
item.publicData.menuPath = menuPath;
item.publicData.menuAvailable = !menuPath.empty() && menuPath != "/";
item.publicData.menuAvailable = !menuPath.empty();
if (menuPathChanged || !item.publicData.menuAvailable)
{
item.menuModel.reset();
item.menuActions.reset();
}
item.publicData.iconName = (status == "NeedsAttention" && !attentionIconName.empty()) ? attentionIconName : iconName;
if (status == "NeedsAttention" && attentionTexture)