diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f998f7..7425fe5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,8 @@ target_sources(bar_lib src/widgets/clock.cpp src/widgets/workspaceIndicator.cpp src/services/hyprland.cpp + src/services/tray.cpp + src/widgets/tray.cpp ) include_directories(bar_lib PRIVATE include diff --git a/include/app.hpp b/include/app.hpp index 7ebe347..81e0703 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -6,6 +6,7 @@ #include "glibmm/refptr.h" #include "gtkmm/application.h" #include "services/hyprland.hpp" +#include "services/tray.hpp" class App { public: @@ -16,6 +17,7 @@ private: Glib::RefPtr app; std::vector bars; HyprlandService hyprlandService; + TrayService trayService; void setupServices(); }; \ No newline at end of file diff --git a/include/bar/bar.hpp b/include/bar/bar.hpp index 2a0c3ee..ecdd93f 100644 --- a/include/bar/bar.hpp +++ b/include/bar/bar.hpp @@ -4,13 +4,15 @@ #include #include "services/hyprland.hpp" +#include "services/tray.hpp" #include "widgets/clock.hpp" #include "widgets/workspaceIndicator.hpp" +#include "widgets/tray.hpp" class Bar : public Gtk::Window { public: - Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, int monitorId); + Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, TrayService &trayService, int monitorId); protected: Clock clock; @@ -21,8 +23,10 @@ class Bar : public Gtk::Window private: HyprlandService &m_hyprlandService; + TrayService &m_trayService; int m_monitorId; WorkspaceIndicator *m_workspaceIndicator = nullptr; + TrayWidget *m_trayWidget = nullptr; void setup_ui(); void load_css(); diff --git a/include/services/tray.hpp b/include/services/tray.hpp new file mode 100644 index 0000000..bf92aa5 --- /dev/null +++ b/include/services/tray.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +class TrayService +{ + public: + struct Item + { + std::string id; + std::string busName; + std::string objectPath; + std::string title; + std::string status; + std::string iconName; + std::string menuPath; + bool menuAvailable = false; + Glib::RefPtr iconPaintable; + }; + + TrayService(); + ~TrayService(); + + void start(); + void stop(); + + std::vector snapshotItems() const; + const Item *findItem(const std::string &id) const; + + void activate(const std::string &id, int32_t x, int32_t y); + void secondaryActivate(const std::string &id, int32_t x, int32_t y); + void contextMenu(const std::string &id, int32_t x, int32_t y); + + sigc::signal &signal_item_added(); + sigc::signal &signal_item_removed(); + sigc::signal &signal_item_updated(); + + private: + struct TrackedItem + { + Item publicData; + guint signalSubscriptionId = 0; + guint ownerWatchId = 0; + }; + + Glib::RefPtr m_connection; + Glib::RefPtr m_nodeInfo; + Gio::DBus::InterfaceVTable m_vtable; + + guint m_nameOwnerId = 0; + guint m_registrationId = 0; + bool m_hostRegistered = false; + + std::map> m_items; + + sigc::signal m_itemAddedSignal; + sigc::signal m_itemRemovedSignal; + sigc::signal m_itemUpdatedSignal; + + void on_bus_acquired(const Glib::RefPtr &connection, const Glib::ustring &name); + void on_name_acquired(const Glib::RefPtr &connection, const Glib::ustring &name); + void on_name_lost(const Glib::RefPtr &connection, const Glib::ustring &name); + + void handle_method_call(const Glib::RefPtr &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 &invocation); + + void handle_get_property_slot(Glib::VariantBase &result, + const Glib::RefPtr &connection, + const Glib::ustring &sender, + const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &property_name); + bool handle_set_property_slot(const Glib::RefPtr &connection, + const Glib::ustring &sender, + const Glib::ustring &object_path, + const Glib::ustring &interface_name, + const Glib::ustring &property_name, + const Glib::VariantBase &value); + + Glib::VariantBase handle_get_property(const Glib::ustring &property_name); + bool handle_set_property(const Glib::ustring &property_name, const Glib::VariantBase &value); + + void register_item(const Glib::ustring &sender, const std::string &service); + void unregister_item(const std::string &id); + + void refresh_item(TrackedItem &item); + void emit_registered_items_changed(); + + Glib::Variant> create_registered_items_variant() const; + + void emit_watcher_signal(const Glib::ustring &signal_name, const Glib::VariantContainerBase ¶meters); + + static void 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); + void on_dbus_signal(const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters); + static void on_name_vanished_static(GDBusConnection *connection, + const gchar *name, + gpointer user_data); + void on_name_vanished(const gchar *bus_name); + + static Glib::RefPtr parse_icon_pixmap(GVariant *variant); +}; diff --git a/include/widgets/tray.hpp b/include/widgets/tray.hpp new file mode 100644 index 0000000..716c6e4 --- /dev/null +++ b/include/widgets/tray.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "services/tray.hpp" + +class TrayIconWidget : public Gtk::Button +{ + public: + TrayIconWidget(TrayService &service, std::string id); + + void update(const TrayService::Item &item); + + private: + TrayService &m_service; + std::string m_id; + Gtk::Box m_container; + Gtk::Picture m_picture; + Gtk::Image m_image; + Glib::RefPtr m_secondaryGesture; + + void on_primary_clicked(); + void on_secondary_released(int n_press, double x, double y); +}; + +class TrayWidget : public Gtk::Box +{ + public: + explicit TrayWidget(TrayService &service); + ~TrayWidget() override; + + private: + TrayService &m_service; + std::map> m_icons; + + sigc::connection m_addConnection; + sigc::connection m_removeConnection; + sigc::connection m_updateConnection; + + void on_item_added(const TrayService::Item &item); + void on_item_removed(const std::string &id); + void on_item_updated(const TrayService::Item &item); + + void rebuild_existing(); +}; diff --git a/src/app.cpp b/src/app.cpp index 03852dd..f032609 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -26,8 +26,7 @@ App::App() { continue; } - auto bar = new Bar(monitor->gobj(), this->hyprlandService, hyprlandMonitor->id); - this->hyprlandService.printMonitor(*hyprlandMonitor); // Debugging output + auto bar = new Bar(monitor->gobj(), this->hyprlandService, this->trayService, hyprlandMonitor->id); bar->set_application(app); bar->show(); @@ -41,6 +40,8 @@ App::App() { delete bar; } bars.clear(); + + this->trayService.stop(); }); } @@ -49,6 +50,7 @@ void App::setupServices() { this->hyprlandService, &HyprlandService::on_hyprland_event)); this->hyprlandService.start(); + this->trayService.start(); } int App::run() { diff --git a/src/bar/bar.cpp b/src/bar/bar.cpp index 2f6aab8..bf804fd 100644 --- a/src/bar/bar.cpp +++ b/src/bar/bar.cpp @@ -8,8 +8,8 @@ #include "glibmm/main.h" #include "sigc++/functors/mem_fun.h" -Bar::Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, int monitorId) - : m_hyprlandService(hyprlandService), m_monitorId(monitorId) +Bar::Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, TrayService &trayService, int monitorId) + : m_hyprlandService(hyprlandService), m_trayService(trayService), m_monitorId(monitorId) { gtk_layer_init_for_window(this->gobj()); @@ -50,20 +50,17 @@ void Bar::setup_ui() left_box.set_margin_start(12); left_box.set_margin_end(12); left_box.set_valign(Gtk::Align::CENTER); - left_box.set_hexpand(false); center_box.set_spacing(6); center_box.set_hexpand(true); center_box.set_margin_top(2); center_box.set_margin_bottom(2); center_box.set_valign(Gtk::Align::CENTER); - center_box.set_halign(Gtk::Align::CENTER); right_box.set_spacing(6); right_box.set_margin_start(12); right_box.set_margin_end(12); right_box.set_valign(Gtk::Align::CENTER); - right_box.set_hexpand(false); m_workspaceIndicator = Gtk::make_managed(m_hyprlandService, m_monitorId); left_box.append(*m_workspaceIndicator); @@ -72,6 +69,9 @@ void Bar::setup_ui() clock.set_halign(Gtk::Align::CENTER); clock.set_valign(Gtk::Align::CENTER); center_box.append(clock); + + m_trayWidget = Gtk::make_managed(m_trayService); + right_box.append(*m_trayWidget); } void Bar::load_css() @@ -79,13 +79,16 @@ void Bar::load_css() auto css_provider = Gtk::CssProvider::create(); css_provider->load_from_data(R"( - window { 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; } .workspace-pill-focused { background-color: #82e9de; color: #111; } .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 new file mode 100644 index 0000000..e1c149f --- /dev/null +++ b/src/services/tray.cpp @@ -0,0 +1,892 @@ +#include "services/tray.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +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"; + +const char *kWatcherIntrospection = R"( + + + + + + + + + + + + + + + + + + + +)"; + +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; +} + +} // namespace + +TrayService::TrayService() + : m_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 (m_nameOwnerId != 0) + { + return; + } + + try + { + Gio::init(); + } + catch (const Glib::Error &) + { + // Already initialised; ignore. + } + + if (!m_nodeInfo) + { + try + { + m_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; + } + } + + m_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 (m_connection) + { + for (auto &pair : m_items) + { + if (pair.second->signalSubscriptionId != 0) + { + g_dbus_connection_signal_unsubscribe(m_connection->gobj(), pair.second->signalSubscriptionId); + } + + if (pair.second->ownerWatchId != 0) + { + g_bus_unwatch_name(pair.second->ownerWatchId); + } + } + } + + m_items.clear(); + + if (m_connection && m_registrationId != 0) + { + m_connection->unregister_object(m_registrationId); + m_registrationId = 0; + } + + if (m_nameOwnerId != 0) + { + Gio::DBus::unown_name(m_nameOwnerId); + m_nameOwnerId = 0; + } + + m_connection.reset(); + m_hostRegistered = false; +} + +std::vector TrayService::snapshotItems() const +{ + std::vector result; + result.reserve(m_items.size()); + + for (const auto &pair : m_items) + { + result.push_back(pair.second->publicData); + } + + return result; +} + +const TrayService::Item *TrayService::findItem(const std::string &id) const +{ + auto it = m_items.find(id); + if (it == m_items.end()) + { + return nullptr; + } + + return &it->second->publicData; +} + +void TrayService::activate(const std::string &id, int32_t x, int32_t y) +{ + auto it = m_items.find(id); + if (it == m_items.end() || !m_connection) + { + return; + } + + GError *error = nullptr; + GVariant *result = g_dbus_connection_call_sync(m_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, + -1, + nullptr, + &error); + + if (result) + { + g_variant_unref(result); + } + + if (error) + { + std::cerr << "[TrayService] Activate failed for " << id << ": " << error->message << std::endl; + g_error_free(error); + } +} + +void TrayService::secondaryActivate(const std::string &id, int32_t x, int32_t y) +{ + auto it = m_items.find(id); + if (it == m_items.end() || !m_connection) + { + return; + } + + GError *error = nullptr; + GVariant *result = g_dbus_connection_call_sync(m_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, + -1, + nullptr, + &error); + + if (result) + { + g_variant_unref(result); + } + + if (error) + { + std::cerr << "[TrayService] SecondaryActivate failed for " << id << ": " << error->message << std::endl; + g_error_free(error); + } +} + +void TrayService::contextMenu(const std::string &id, int32_t x, int32_t y) +{ + auto it = m_items.find(id); + if (it == m_items.end() || !m_connection) + { + return; + } + + GError *error = nullptr; + GVariant *result = g_dbus_connection_call_sync(m_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, + -1, + nullptr, + &error); + + if (result) + { + g_variant_unref(result); + } + + if (error) + { + std::cerr << "[TrayService] ContextMenu failed for " << id << ": " << error->message << std::endl; + g_error_free(error); + } +} + +sigc::signal &TrayService::signal_item_added() +{ + return m_itemAddedSignal; +} + +sigc::signal &TrayService::signal_item_removed() +{ + return m_itemRemovedSignal; +} + +sigc::signal &TrayService::signal_item_updated() +{ + return m_itemUpdatedSignal; +} + +void TrayService::on_bus_acquired(const Glib::RefPtr &connection, const Glib::ustring &) +{ + m_connection = connection; + + auto interface_info = m_nodeInfo->lookup_interface(kWatcherInterface); + if (!interface_info) + { + std::cerr << "[TrayService] Missing interface info for watcher." << std::endl; + return; + } + + try + { + m_registrationId = connection->register_object(kWatcherObjectPath, interface_info, m_vtable); + } + catch (const Glib::Error &err) + { + std::cerr << "[TrayService] Failed to register watcher object: " << err.what() << std::endl; + m_registrationId = 0; + return; + } + + m_hostRegistered = true; + emit_watcher_signal("StatusNotifierHostRegistered", Glib::VariantContainerBase()); +} + +void TrayService::on_name_acquired(const Glib::RefPtr & /*connection*/, const Glib::ustring &) +{ +} + +void TrayService::on_name_lost(const Glib::RefPtr & /*connection*/, const Glib::ustring &) +{ + stop(); +} + +void TrayService::handle_method_call(const Glib::RefPtr & /*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 &invocation) +{ + if (method_name == "RegisterStatusNotifierItem") + { + Glib::ustring service; + if (auto rawParams = parameters.gobj()) + { + GVariant *child = g_variant_get_child_value(const_cast(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") + { + m_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 & /*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 & /*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::create(m_hostRegistered); + } + if (property_name == "ProtocolVersion") + { + return Glib::Variant::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 (!m_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 = m_items.find(id); + if (existing != m_items.end()) + { + refresh_item(*existing->second); + m_itemUpdatedSignal.emit(existing->second->publicData); + return; + } + + auto item = std::make_unique(); + item->publicData.id = id; + item->publicData.busName = parsed.busName; + item->publicData.objectPath = parsed.objectPath; + + refresh_item(*item); + + item->signalSubscriptionId = g_dbus_connection_signal_subscribe(m_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(m_connection->gobj(), + item->publicData.busName.c_str(), + G_BUS_NAME_WATCHER_FLAGS_NONE, + nullptr, + &TrayService::on_name_vanished_static, + this, + nullptr); + + m_items.emplace(id, std::move(item)); + + emit_registered_items_changed(); + + auto params = Glib::Variant>::create(std::make_tuple(Glib::ustring(id))); + emit_watcher_signal("StatusNotifierItemRegistered", params); + + m_itemAddedSignal.emit(m_items.at(id)->publicData); +} + +void TrayService::unregister_item(const std::string &id) +{ + auto it = m_items.find(id); + if (it == m_items.end()) + { + return; + } + + if (m_connection && it->second->signalSubscriptionId != 0) + { + g_dbus_connection_signal_unsubscribe(m_connection->gobj(), it->second->signalSubscriptionId); + } + + if (it->second->ownerWatchId != 0) + { + g_bus_unwatch_name(it->second->ownerWatchId); + } + + m_items.erase(it); + + emit_registered_items_changed(); + + auto params = Glib::Variant>::create(std::make_tuple(Glib::ustring(id))); + emit_watcher_signal("StatusNotifierItemUnregistered", params); + + m_itemRemovedSignal.emit(id); +} + +void TrayService::refresh_item(TrackedItem &item) +{ + if (!m_connection) + { + return; + } + + GError *error = nullptr; + GVariant *reply = g_dbus_connection_call_sync(m_connection->gobj(), + item.publicData.busName.c_str(), + item.publicData.objectPath.c_str(), + kDBusPropertiesIface, + "GetAll", + g_variant_new("(s)", kItemInterface), + G_VARIANT_TYPE("(a{sv})"), + G_DBUS_CALL_FLAGS_NONE, + -1, + nullptr, + &error); + if (!reply) + { + if (error) + { + std::cerr << "[TrayService] Failed to query properties for " << item.publicData.id << ": " << error->message << std::endl; + g_error_free(error); + } + return; + } + + GVariant *dictVariant = g_variant_get_child_value(reply, 0); + g_variant_unref(reply); + + if (!dictVariant) + { + return; + } + + GVariantIter iter; + g_variant_iter_init(&iter, dictVariant); + + const gchar *key = nullptr; + GVariant *value = nullptr; + + Glib::RefPtr iconTexture; + Glib::RefPtr 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) + { + if (g_variant_is_of_type(value, G_VARIANT_TYPE_OBJECT_PATH)) + { + const gchar *str = g_variant_get_string(value, nullptr); + menuPath = str ? str : ""; + } + else + { + 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 = parse_icon_pixmap(value); + } + else if (std::strcmp(key, "AttentionIconPixmap") == 0) + { + attentionTexture = parse_icon_pixmap(value); + } + + g_variant_unref(value); + } + + g_variant_unref(dictVariant); + + item.publicData.title = title; + item.publicData.status = status; + item.publicData.menuPath = menuPath; + item.publicData.menuAvailable = !menuPath.empty() && menuPath != "/"; + item.publicData.iconName = (status == "NeedsAttention" && !attentionIconName.empty()) ? attentionIconName : iconName; + + if (status == "NeedsAttention" && attentionTexture) + { + item.publicData.iconPaintable = attentionTexture; + } + else + { + item.publicData.iconPaintable = iconTexture; + } + + if (!item.publicData.iconPaintable && iconTexture) + { + item.publicData.iconPaintable = iconTexture; + } +} + +void TrayService::emit_registered_items_changed() +{ + if (!m_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(m_connection->gobj(), + nullptr, + kWatcherObjectPath, + kDBusPropertiesIface, + "PropertiesChanged", + params, + nullptr); +} + +Glib::Variant> TrayService::create_registered_items_variant() const +{ + std::vector values; + values.reserve(m_items.size()); + + for (const auto &pair : m_items) + { + values.emplace_back(pair.second->publicData.id); + } + + return Glib::Variant>::create(values); +} + +void TrayService::emit_watcher_signal(const Glib::ustring &signal_name, const Glib::VariantContainerBase ¶meters) +{ + if (!m_connection) + { + return; + } + + GVariant *payload = parameters.gobj() ? parameters.gobj_copy() : nullptr; + + g_dbus_connection_emit_signal(m_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(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(m_items.begin(), m_items.end(), [&](const auto &pair) { + return pair.second->publicData.busName == sender_name && pair.second->publicData.objectPath == object_path; + }); + + if (it == m_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) + { + refresh_item(*it->second); + m_itemUpdatedSignal.emit(it->second->publicData); + } + } + else if (isPropertiesSignal) + { + refresh_item(*it->second); + m_itemUpdatedSignal.emit(it->second->publicData); + } +} + +void TrayService::on_name_vanished_static(GDBusConnection * /*connection*/, const gchar *name, gpointer user_data) +{ + if (auto *self = static_cast(user_data)) + { + self->on_name_vanished(name); + } +} + +void TrayService::on_name_vanished(const gchar *bus_name) +{ + if (!bus_name) + { + return; + } + + std::vector toRemove; + for (const auto &pair : m_items) + { + if (pair.second->publicData.busName == bus_name) + { + toRemove.push_back(pair.first); + } + } + + for (const auto &id : toRemove) + { + unregister_item(id); + } +} + +Glib::RefPtr 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(g_variant_get_fixed_array(bytesVariant, &rawLength, sizeof(guint8))); + if (!rawBytes || rawLength < static_cast(width * height * 4)) + { + g_variant_unref(bytesVariant); + g_variant_unref(entry); + return {}; + } + + std::vector rgba; + const std::size_t pixelCount = static_cast(width) * static_cast(height); + rgba.resize(pixelCount * 4); + + const guint32 *pixels = reinterpret_cast(rawBytes); + for (std::size_t idx = 0; idx < pixelCount; ++idx) + { + const guint32 pixel = pixels[idx]; + const guint8 a = static_cast((pixel >> 24) & 0xFF); + const guint8 r = static_cast((pixel >> 16) & 0xFF); + const guint8 g = static_cast((pixel >> 8) & 0xFF); + const guint8 b = static_cast(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(y) * srcRowstride, static_cast(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 {}; + } +} diff --git a/src/widgets/tray.cpp b/src/widgets/tray.cpp new file mode 100644 index 0000000..6cc4a1d --- /dev/null +++ b/src/widgets/tray.cpp @@ -0,0 +1,174 @@ +#include "widgets/tray.hpp" + +#include + +TrayIconWidget::TrayIconWidget(TrayService &service, std::string id) + : m_service(service), m_id(std::move(id)), m_container(Gtk::Orientation::HORIZONTAL) +{ + set_has_frame(false); + set_focusable(false); + set_valign(Gtk::Align::CENTER); + set_halign(Gtk::Align::CENTER); + add_css_class("tray-icon"); + + m_picture.set_halign(Gtk::Align::CENTER); + m_picture.set_valign(Gtk::Align::CENTER); + m_picture.set_content_fit(Gtk::ContentFit::CONTAIN); + m_picture.set_can_shrink(true); + m_picture.set_size_request(20, 20); + + m_image.set_pixel_size(20); + m_image.set_halign(Gtk::Align::CENTER); + m_image.set_valign(Gtk::Align::CENTER); + + m_container.set_spacing(0); + m_container.set_halign(Gtk::Align::CENTER); + m_container.set_valign(Gtk::Align::CENTER); + m_container.append(m_picture); + m_container.append(m_image); + + m_picture.set_visible(false); + m_image.set_visible(true); + set_child(m_container); + + signal_clicked().connect(sigc::mem_fun(*this, &TrayIconWidget::on_primary_clicked)); + + m_secondaryGesture = Gtk::GestureClick::create(); + m_secondaryGesture->set_button(GDK_BUTTON_SECONDARY); + m_secondaryGesture->signal_released().connect(sigc::mem_fun(*this, &TrayIconWidget::on_secondary_released)); + add_controller(m_secondaryGesture); +} + +void TrayIconWidget::update(const TrayService::Item &item) +{ + if (item.iconPaintable) + { + m_picture.set_paintable(item.iconPaintable); + m_picture.set_visible(true); + m_image.set_visible(false); + } + else if (!item.iconName.empty()) + { + m_image.set_from_icon_name(item.iconName); + m_image.set_pixel_size(20); + m_image.set_visible(true); + m_picture.set_visible(false); + } + else + { + m_picture.set_paintable({}); + m_image.set_visible(false); + m_picture.set_visible(false); + } + + if (!item.title.empty()) + { + set_tooltip_text(item.title); + } + else + { + set_tooltip_text(""); + } + + set_sensitive(item.status != "Passive"); +} + +void TrayIconWidget::on_primary_clicked() +{ + m_service.activate(m_id, 0, 0); +} + +void TrayIconWidget::on_secondary_released(int /*n_press*/, double /*x*/, double /*y*/) +{ + m_service.contextMenu(m_id, 0, 0); +} + +TrayWidget::TrayWidget(TrayService &service) + : Gtk::Box(Gtk::Orientation::HORIZONTAL), m_service(service) +{ + set_spacing(6); + set_valign(Gtk::Align::CENTER); + set_halign(Gtk::Align::CENTER); + set_visible(false); + + m_addConnection = m_service.signal_item_added().connect(sigc::mem_fun(*this, &TrayWidget::on_item_added)); + m_removeConnection = m_service.signal_item_removed().connect(sigc::mem_fun(*this, &TrayWidget::on_item_removed)); + m_updateConnection = m_service.signal_item_updated().connect(sigc::mem_fun(*this, &TrayWidget::on_item_updated)); + + rebuild_existing(); +} + +TrayWidget::~TrayWidget() +{ + if (m_addConnection.connected()) + { + m_addConnection.disconnect(); + } + if (m_removeConnection.connected()) + { + m_removeConnection.disconnect(); + } + if (m_updateConnection.connected()) + { + m_updateConnection.disconnect(); + } +} + +void TrayWidget::rebuild_existing() +{ + auto items = m_service.snapshotItems(); + for (const auto &item : items) + { + on_item_added(item); + } + + set_visible(!m_icons.empty()); +} + +void TrayWidget::on_item_added(const TrayService::Item &item) +{ + auto it = m_icons.find(item.id); + if (it != m_icons.end()) + { + it->second->update(item); + return; + } + + auto icon = std::make_unique(m_service, item.id); + icon->update(item); + auto *raw = icon.get(); + append(*raw); + m_icons.emplace(item.id, std::move(icon)); + + set_visible(true); +} + +void TrayWidget::on_item_removed(const std::string &id) +{ + auto it = m_icons.find(id); + if (it == m_icons.end()) + { + return; + } + + remove(*it->second); + it->second->unparent(); + m_icons.erase(it); + + if (m_icons.empty()) + { + set_visible(false); + } +} + +void TrayWidget::on_item_updated(const TrayService::Item &item) +{ + auto it = m_icons.find(item.id); + if (it == m_icons.end()) + { + on_item_added(item); + return; + } + + it->second->update(item); +}