add tray icons
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<Gtk::Application> app;
|
||||
std::vector<Bar*> bars;
|
||||
HyprlandService hyprlandService;
|
||||
TrayService trayService;
|
||||
|
||||
void setupServices();
|
||||
};
|
||||
@@ -4,13 +4,15 @@
|
||||
#include <gtkmm.h>
|
||||
|
||||
#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();
|
||||
|
||||
127
include/services/tray.hpp
Normal file
127
include/services/tray.hpp
Normal 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 ¶meters,
|
||||
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 ¶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<Gdk::Paintable> parse_icon_pixmap(GVariant *variant);
|
||||
};
|
||||
53
include/widgets/tray.hpp
Normal file
53
include/widgets/tray.hpp
Normal 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();
|
||||
};
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<WorkspaceIndicator>(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<TrayWidget>(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(
|
||||
|
||||
892
src/services/tray.cpp
Normal file
892
src/services/tray.cpp
Normal 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 ¶meters,
|
||||
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 ¶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<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
174
src/widgets/tray.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user