add tray icons
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user