add tray icons

This commit is contained in:
2025-12-10 00:25:49 +01:00
parent d53dfa27f1
commit 7bd4c72763
9 changed files with 1268 additions and 9 deletions

View File

@@ -23,6 +23,8 @@ target_sources(bar_lib
src/widgets/clock.cpp src/widgets/clock.cpp
src/widgets/workspaceIndicator.cpp src/widgets/workspaceIndicator.cpp
src/services/hyprland.cpp src/services/hyprland.cpp
src/services/tray.cpp
src/widgets/tray.cpp
) )
include_directories(bar_lib PRIVATE include_directories(bar_lib PRIVATE
include include

View File

@@ -6,6 +6,7 @@
#include "glibmm/refptr.h" #include "glibmm/refptr.h"
#include "gtkmm/application.h" #include "gtkmm/application.h"
#include "services/hyprland.hpp" #include "services/hyprland.hpp"
#include "services/tray.hpp"
class App { class App {
public: public:
@@ -16,6 +17,7 @@ private:
Glib::RefPtr<Gtk::Application> app; Glib::RefPtr<Gtk::Application> app;
std::vector<Bar*> bars; std::vector<Bar*> bars;
HyprlandService hyprlandService; HyprlandService hyprlandService;
TrayService trayService;
void setupServices(); void setupServices();
}; };

View File

@@ -4,13 +4,15 @@
#include <gtkmm.h> #include <gtkmm.h>
#include "services/hyprland.hpp" #include "services/hyprland.hpp"
#include "services/tray.hpp"
#include "widgets/clock.hpp" #include "widgets/clock.hpp"
#include "widgets/workspaceIndicator.hpp" #include "widgets/workspaceIndicator.hpp"
#include "widgets/tray.hpp"
class Bar : public Gtk::Window class Bar : public Gtk::Window
{ {
public: public:
Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, int monitorId); Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, TrayService &trayService, int monitorId);
protected: protected:
Clock clock; Clock clock;
@@ -21,8 +23,10 @@ class Bar : public Gtk::Window
private: private:
HyprlandService &m_hyprlandService; HyprlandService &m_hyprlandService;
TrayService &m_trayService;
int m_monitorId; int m_monitorId;
WorkspaceIndicator *m_workspaceIndicator = nullptr; WorkspaceIndicator *m_workspaceIndicator = nullptr;
TrayWidget *m_trayWidget = nullptr;
void setup_ui(); void setup_ui();
void load_css(); void load_css();

127
include/services/tray.hpp Normal file
View File

@@ -0,0 +1,127 @@
#pragma once
#include <gdkmm/memorytexture.h>
#include <giomm/dbusconnection.h>
#include <giomm/init.h>
#include <glibmm/bytes.h>
#include <glibmm/refptr.h>
#include <sigc++/sigc++.h>
#include <gio/gio.h>
#include <map>
#include <memory>
#include <string>
#include <vector>
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<Gdk::Paintable> iconPaintable;
};
TrayService();
~TrayService();
void start();
void stop();
std::vector<Item> 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<void(const Item &)> &signal_item_added();
sigc::signal<void(const std::string &)> &signal_item_removed();
sigc::signal<void(const Item &)> &signal_item_updated();
private:
struct TrackedItem
{
Item publicData;
guint signalSubscriptionId = 0;
guint ownerWatchId = 0;
};
Glib::RefPtr<Gio::DBus::Connection> m_connection;
Glib::RefPtr<Gio::DBus::NodeInfo> m_nodeInfo;
Gio::DBus::InterfaceVTable m_vtable;
guint m_nameOwnerId = 0;
guint m_registrationId = 0;
bool m_hostRegistered = false;
std::map<std::string, std::unique_ptr<TrackedItem>> m_items;
sigc::signal<void(const Item &)> m_itemAddedSignal;
sigc::signal<void(const std::string &)> m_itemRemovedSignal;
sigc::signal<void(const Item &)> m_itemUpdatedSignal;
void on_bus_acquired(const Glib::RefPtr<Gio::DBus::Connection> &connection, const Glib::ustring &name);
void on_name_acquired(const Glib::RefPtr<Gio::DBus::Connection> &connection, const Glib::ustring &name);
void on_name_lost(const Glib::RefPtr<Gio::DBus::Connection> &connection, const Glib::ustring &name);
void 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 &parameters,
const Glib::RefPtr<Gio::DBus::MethodInvocation> &invocation);
void 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);
bool 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);
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<std::vector<Glib::ustring>> create_registered_items_variant() const;
void emit_watcher_signal(const Glib::ustring &signal_name, const Glib::VariantContainerBase &parameters);
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<Gdk::Paintable> parse_icon_pixmap(GVariant *variant);
};

53
include/widgets/tray.hpp Normal file
View File

@@ -0,0 +1,53 @@
#pragma once
#include <gtkmm/box.h>
#include <gtkmm/button.h>
#include <gtkmm/gestureclick.h>
#include <gtkmm/picture.h>
#include <gtkmm/image.h>
#include <map>
#include <memory>
#include <string>
#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<Gtk::GestureClick> 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<std::string, std::unique_ptr<TrayIconWidget>> 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();
};

View File

@@ -26,8 +26,7 @@ App::App() {
continue; continue;
} }
auto bar = new Bar(monitor->gobj(), this->hyprlandService, hyprlandMonitor->id); auto bar = new Bar(monitor->gobj(), this->hyprlandService, this->trayService, hyprlandMonitor->id);
this->hyprlandService.printMonitor(*hyprlandMonitor); // Debugging output
bar->set_application(app); bar->set_application(app);
bar->show(); bar->show();
@@ -41,6 +40,8 @@ App::App() {
delete bar; delete bar;
} }
bars.clear(); bars.clear();
this->trayService.stop();
}); });
} }
@@ -49,6 +50,7 @@ void App::setupServices() {
this->hyprlandService, &HyprlandService::on_hyprland_event)); this->hyprlandService, &HyprlandService::on_hyprland_event));
this->hyprlandService.start(); this->hyprlandService.start();
this->trayService.start();
} }
int App::run() { int App::run() {

View File

@@ -8,8 +8,8 @@
#include "glibmm/main.h" #include "glibmm/main.h"
#include "sigc++/functors/mem_fun.h" #include "sigc++/functors/mem_fun.h"
Bar::Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, int monitorId) Bar::Bar(GdkMonitor *monitor, HyprlandService &hyprlandService, TrayService &trayService, int monitorId)
: m_hyprlandService(hyprlandService), m_monitorId(monitorId) : m_hyprlandService(hyprlandService), m_trayService(trayService), m_monitorId(monitorId)
{ {
gtk_layer_init_for_window(this->gobj()); 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_start(12);
left_box.set_margin_end(12); left_box.set_margin_end(12);
left_box.set_valign(Gtk::Align::CENTER); left_box.set_valign(Gtk::Align::CENTER);
left_box.set_hexpand(false);
center_box.set_spacing(6); center_box.set_spacing(6);
center_box.set_hexpand(true); center_box.set_hexpand(true);
center_box.set_margin_top(2); center_box.set_margin_top(2);
center_box.set_margin_bottom(2); center_box.set_margin_bottom(2);
center_box.set_valign(Gtk::Align::CENTER); center_box.set_valign(Gtk::Align::CENTER);
center_box.set_halign(Gtk::Align::CENTER);
right_box.set_spacing(6); right_box.set_spacing(6);
right_box.set_margin_start(12); right_box.set_margin_start(12);
right_box.set_margin_end(12); right_box.set_margin_end(12);
right_box.set_valign(Gtk::Align::CENTER); right_box.set_valign(Gtk::Align::CENTER);
right_box.set_hexpand(false);
m_workspaceIndicator = Gtk::make_managed<WorkspaceIndicator>(m_hyprlandService, m_monitorId); m_workspaceIndicator = Gtk::make_managed<WorkspaceIndicator>(m_hyprlandService, m_monitorId);
left_box.append(*m_workspaceIndicator); left_box.append(*m_workspaceIndicator);
@@ -72,6 +69,9 @@ void Bar::setup_ui()
clock.set_halign(Gtk::Align::CENTER); clock.set_halign(Gtk::Align::CENTER);
clock.set_valign(Gtk::Align::CENTER); clock.set_valign(Gtk::Align::CENTER);
center_box.append(clock); center_box.append(clock);
m_trayWidget = Gtk::make_managed<TrayWidget>(m_trayService);
right_box.append(*m_trayWidget);
} }
void Bar::load_css() void Bar::load_css()
@@ -79,13 +79,16 @@ void Bar::load_css()
auto css_provider = Gtk::CssProvider::create(); auto css_provider = Gtk::CssProvider::create();
css_provider->load_from_data(R"( 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; } #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 { 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:last-child { margin-right: 0; }
.workspace-pill-focused { background-color: #82e9de; color: #111; } .workspace-pill-focused { background-color: #82e9de; color: #111; }
.workspace-pill-active { background-color: rgba(255, 255, 255, 0.25); } .workspace-pill-active { background-color: rgba(255, 255, 255, 0.25); }
.workspace-pill-urgent { background-color: #ff5555; color: #111; } .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( Gtk::StyleContext::add_provider_for_display(

892
src/services/tray.cpp Normal file
View File

@@ -0,0 +1,892 @@
#include "services/tray.hpp"
#include <giomm/dbusownname.h>
#include <gdkmm/pixbuf.h>
#include <gdkmm/texture.h>
#include <algorithm>
#include <cstring>
#include <iostream>
#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";
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;
}
} // 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::Item> TrayService::snapshotItems() const
{
std::vector<Item> 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<void(const TrayService::Item &)> &TrayService::signal_item_added()
{
return m_itemAddedSignal;
}
sigc::signal<void(const std::string &)> &TrayService::signal_item_removed()
{
return m_itemRemovedSignal;
}
sigc::signal<void(const TrayService::Item &)> &TrayService::signal_item_updated()
{
return m_itemUpdatedSignal;
}
void TrayService::on_bus_acquired(const Glib::RefPtr<Gio::DBus::Connection> &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<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 &parameters,
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")
{
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<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(m_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 (!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<TrackedItem>();
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<std::tuple<Glib::ustring>>::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<std::tuple<Glib::ustring>>::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<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)
{
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<std::vector<Glib::ustring>> TrayService::create_registered_items_variant() const
{
std::vector<Glib::ustring> values;
values.reserve(m_items.size());
for (const auto &pair : m_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 &parameters)
{
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<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(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<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 : m_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)
{
std::cerr << "[TrayService] Failed to create texture: " << err.what() << std::endl;
return {};
}
}

174
src/widgets/tray.cpp Normal file
View File

@@ -0,0 +1,174 @@
#include "widgets/tray.hpp"
#include <utility>
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<TrayIconWidget>(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);
}