From 70a271fb8b1e64d29a9d625e15542ecb2170d38b Mon Sep 17 00:00:00 2001 From: Arif Hasanic Date: Wed, 10 Dec 2025 01:35:39 +0100 Subject: [PATCH] add tray icons --- include/services/tray.hpp | 20 ++ include/widgets/tray.hpp | 20 ++ src/bar/bar.cpp | 4 +- src/services/tray.cpp | 431 +++++++++++++++++++++++++++++++++++++- src/widgets/tray.cpp | 200 +++++++++++++++++- tmp_test.cpp | 10 + 6 files changed, 677 insertions(+), 8 deletions(-) create mode 100644 tmp_test.cpp diff --git a/include/services/tray.hpp b/include/services/tray.hpp index bf92aa5..dab3c47 100644 --- a/include/services/tray.hpp +++ b/include/services/tray.hpp @@ -1,7 +1,9 @@ #pragma once #include +#include #include +#include #include #include #include @@ -10,6 +12,7 @@ #include #include #include +#include #include #include @@ -42,6 +45,21 @@ class TrayService void secondaryActivate(const std::string &id, int32_t x, int32_t y); void contextMenu(const std::string &id, int32_t x, int32_t y); + Glib::RefPtr get_menu_model(const std::string &id); + Glib::RefPtr get_menu_action_group(const std::string &id); + void debug_dump_menu_layout(const std::string &id); + struct MenuNode + { + int id = 0; + std::string label; + bool enabled = true; + bool visible = true; + bool separator = false; + std::vector children; + }; + std::optional get_menu_layout(const std::string &id); + bool activate_menu_item(const std::string &id, int itemId); + sigc::signal &signal_item_added(); sigc::signal &signal_item_removed(); sigc::signal &signal_item_updated(); @@ -52,6 +70,8 @@ class TrayService Item publicData; guint signalSubscriptionId = 0; guint ownerWatchId = 0; + Glib::RefPtr menuModel; + Glib::RefPtr menuActions; }; Glib::RefPtr m_connection; diff --git a/include/widgets/tray.hpp b/include/widgets/tray.hpp index 716c6e4..24e7bf4 100644 --- a/include/widgets/tray.hpp +++ b/include/widgets/tray.hpp @@ -5,6 +5,12 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include @@ -26,9 +32,23 @@ class TrayIconWidget : public Gtk::Button Gtk::Picture m_picture; Gtk::Image m_image; Glib::RefPtr m_secondaryGesture; + Glib::RefPtr m_menuPopover; + Glib::RefPtr m_menuActions; + Glib::RefPtr m_menuModel; + sigc::connection m_menuChangedConnection; + bool m_menuPopupPending = false; + double m_pendingX = 0.0; + double m_pendingY = 0.0; void on_primary_clicked(); void on_secondary_released(int n_press, double x, double y); + bool ensure_menu(); + void on_menu_items_changed(guint position, guint removed, guint added); + void try_popup(); + void populate_menu_items(const std::vector &nodes, + const Glib::RefPtr &menu, + const Glib::RefPtr &actions); + void on_menu_action(const Glib::VariantBase ¶meter, int itemId); }; class TrayWidget : public Gtk::Box diff --git a/src/bar/bar.cpp b/src/bar/bar.cpp index bf804fd..4d65d38 100644 --- a/src/bar/bar.cpp +++ b/src/bar/bar.cpp @@ -79,7 +79,7 @@ void Bar::load_css() auto css_provider = Gtk::CssProvider::create(); css_provider->load_from_data(R"( - window { height: 24px; background-color: #222; color: #fff; } + #window { height: 24px; background-color: #222; color: #fff; } #clock-label { font-weight: bold; font-family: monospace; } .workspace-pill { background-color: rgba(255, 255, 255, 0.12); border-radius: 8px; padding: 2px 8px; margin-right: 6px; } .workspace-pill:last-child { margin-right: 0; } @@ -87,8 +87,6 @@ void Bar::load_css() .workspace-pill-active { background-color: rgba(255, 255, 255, 0.25); } .workspace-pill-urgent { background-color: #ff5555; color: #111; } .tray-icon { padding: 0; margin: 0; border: none; background: transparent; min-width: 0; min-height: 0; } - .tray-icon image, - .tray-icon picture { margin: 0; min-width: 0; min-height: 0; } )"); Gtk::StyleContext::add_provider_for_display( diff --git a/src/services/tray.cpp b/src/services/tray.cpp index e1c149f..cf71fc6 100644 --- a/src/services/tray.cpp +++ b/src/services/tray.cpp @@ -1,8 +1,11 @@ #include "services/tray.hpp" +#include #include +#include #include #include +#include #include #include @@ -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"( @@ -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 &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 &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 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 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::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(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 &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) diff --git a/src/widgets/tray.cpp b/src/widgets/tray.cpp index 6cc4a1d..7401bf7 100644 --- a/src/widgets/tray.cpp +++ b/src/widgets/tray.cpp @@ -1,6 +1,11 @@ #include "widgets/tray.hpp" +#include +#include +#include +#include #include +#include TrayIconWidget::TrayIconWidget(TrayService &service, std::string id) : m_service(service), m_id(std::move(id)), m_container(Gtk::Orientation::HORIZONTAL) @@ -41,6 +46,24 @@ TrayIconWidget::TrayIconWidget(TrayService &service, std::string id) void TrayIconWidget::update(const TrayService::Item &item) { + if (!item.menuAvailable) + { + m_menuModel.reset(); + m_menuActions.reset(); + m_menuPopupPending = false; + if (m_menuChangedConnection.connected()) + { + m_menuChangedConnection.disconnect(); + } + if (m_menuPopover) + { + m_menuPopover->insert_action_group("dbusmenu", Glib::RefPtr()); + m_menuPopover->set_menu_model({}); + m_menuPopover->unparent(); + m_menuPopover.reset(); + } + } + if (item.iconPaintable) { m_picture.set_paintable(item.iconPaintable); @@ -78,15 +101,186 @@ void TrayIconWidget::on_primary_clicked() m_service.activate(m_id, 0, 0); } -void TrayIconWidget::on_secondary_released(int /*n_press*/, double /*x*/, double /*y*/) +void TrayIconWidget::on_secondary_released(int /*n_press*/, double x, double y) { - m_service.contextMenu(m_id, 0, 0); + m_service.contextMenu(m_id, static_cast(x), static_cast(y)); + + if (!ensure_menu()) + { + return; + } + + m_pendingX = x; + m_pendingY = y; + m_menuPopupPending = true; + try_popup(); +} + +bool TrayIconWidget::ensure_menu() +{ + auto layoutOpt = m_service.get_menu_layout(m_id); + if (!layoutOpt) + { + m_menuModel.reset(); + m_menuActions.reset(); + m_menuPopupPending = false; + if (m_menuChangedConnection.connected()) + { + m_menuChangedConnection.disconnect(); + } + if (m_menuPopover) + { + remove_action_group("dbusmenu"); + m_menuPopover->set_menu_model({}); + m_menuPopover->unparent(); + m_menuPopover.reset(); + } + return false; + } + + + const auto &layout = *layoutOpt; + + + + + + + auto menu = Gio::Menu::create(); + auto actions = Gio::SimpleActionGroup::create(); + + populate_menu_items(layout.children, menu, actions); + + const auto itemCount = menu->get_n_items(); + std::cout << "[TrayIconWidget] menu update for " << m_id << ", items: " << itemCount << std::endl; + if (itemCount == 0) + { + m_service.debug_dump_menu_layout(m_id); + return false; + } + + m_menuModel = menu; + m_menuActions = actions; + + if (!m_menuPopover) + { + auto *rawPopover = Gtk::make_managed(); + m_menuPopover = Glib::make_refptr_for_instance(rawPopover); + if (!m_menuPopover) + { + return false; + } + + m_menuPopover->set_has_arrow(false); + m_menuPopover->set_autohide(true); + m_menuPopover->set_parent(*this); + } + + m_menuPopover->remove_action_group("dbusmenu"); + m_menuPopover->insert_action_group("dbusmenu", m_menuActions); + + if (m_menuChangedConnection.connected()) + { + m_menuChangedConnection.disconnect(); + } + m_menuChangedConnection = m_menuModel->signal_items_changed().connect(sigc::mem_fun(*this, &TrayIconWidget::on_menu_items_changed)); + + m_menuPopover->set_menu_model(m_menuModel); + + return true; +} + +void TrayIconWidget::on_menu_items_changed(guint /*position*/, guint /*removed*/, guint /*added*/) +{ + if (!m_menuModel) + { + return; + } + + const auto count = m_menuModel->get_n_items(); + std::cout << "[TrayIconWidget] items changed for " << m_id << ": " << count << " entries" << std::endl; + try_popup(); +} + +void TrayIconWidget::try_popup() +{ + if (!m_menuPopupPending || !m_menuPopover || !m_menuModel) + { + return; + } + + if (m_menuModel->get_n_items() == 0) + { + return; + } + + Gdk::Rectangle rect(static_cast(m_pendingX), static_cast(m_pendingY), 1, 1); + m_menuPopover->set_pointing_to(rect); + m_menuPopover->popup(); + m_menuPopupPending = false; +} + +void TrayIconWidget::populate_menu_items(const std::vector &nodes, + const Glib::RefPtr &menu, + const Glib::RefPtr &actions) +{ + for (const auto &node : nodes) + { + if (!node.visible) + { + continue; + } + + if (node.separator) + { + auto section = Gio::Menu::create(); + menu->append_section("", section); + continue; + } + + if (!node.children.empty()) + { + auto submenu = Gio::Menu::create(); + populate_menu_items(node.children, submenu, actions); + auto submenuItem = Gio::MenuItem::create(node.label, Glib::ustring()); + submenuItem->set_submenu(submenu); + if (!node.enabled) + { + submenuItem->set_attribute_value("enabled", Glib::Variant::create(false)); + } + menu->append_item(submenuItem); + continue; + } + + const std::string actionName = "item" + std::to_string(node.id); + auto menuItem = Gio::MenuItem::create(node.label, "dbusmenu." + actionName); + if (!node.enabled) + { + menuItem->set_attribute_value("enabled", Glib::Variant::create(false)); + } + + auto action = Gio::SimpleAction::create(actionName); + action->set_enabled(node.enabled); + action->signal_activate().connect(sigc::bind(sigc::mem_fun(*this, &TrayIconWidget::on_menu_action), node.id)); + actions->add_action(action); + + menu->append_item(menuItem); + } +} + +void TrayIconWidget::on_menu_action(const Glib::VariantBase & /*parameter*/, int itemId) +{ + m_service.activate_menu_item(m_id, itemId); + if (m_menuPopover) + { + m_menuPopover->popdown(); + } } TrayWidget::TrayWidget(TrayService &service) : Gtk::Box(Gtk::Orientation::HORIZONTAL), m_service(service) { - set_spacing(6); + set_spacing(4); set_valign(Gtk::Align::CENTER); set_halign(Gtk::Align::CENTER); set_visible(false); diff --git a/tmp_test.cpp b/tmp_test.cpp new file mode 100644 index 0000000..59d084d --- /dev/null +++ b/tmp_test.cpp @@ -0,0 +1,10 @@ +#include +#include +#include + +int main(){ + GDBusMenuModel *dbusModel = g_dbus_menu_model_get_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_MENU_MODEL_FLAGS_NONE, "org.freedesktop.Notifications", "/Menu", nullptr, nullptr); + if(!dbusModel) return 0; + Glib::RefPtr model = Glib::wrap(G_MENU_MODEL(dbusModel)); + return model ? 0 : 1; +}