service/tray: mostly complete StatusNotifierItem implementation

Notably missing dbusmenu which makes it actually useful.
This commit is contained in:
outfoxxed 2024-04-06 02:19:40 -07:00
parent d47a7f2cff
commit 6214ac1002
Signed by: outfoxxed
GPG Key ID: 4C88A185FB89301E
25 changed files with 1321 additions and 4 deletions

View File

@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON)
option(WAYLAND "Enable wayland support" ON)
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
message(STATUS "Quickshell configuration")
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
@ -24,6 +25,8 @@ if (WAYLAND)
message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}")
message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}")
endif ()
message(STATUS " Services")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
if (NOT DEFINED GIT_REVISION)
execute_process(
@ -73,6 +76,10 @@ if (WAYLAND)
list(APPEND QT_FPDEPS WaylandClient)
endif()
if (SERVICE_STATUS_NOTIFIER)
set(DBUS ON)
endif()
if (DBUS)
list(APPEND QT_DEPS Qt6::DBus)
list(APPEND QT_FPDEPS DBus)

View File

@ -12,3 +12,5 @@ endif()
if (WAYLAND)
add_subdirectory(wayland)
endif ()
add_subdirectory(services)

View File

@ -33,6 +33,8 @@ EngineGeneration::EngineGeneration(QmlScanner scanner)
this->engine.setIncubationController(&this->delayedIncubationController);
this->engine.addImageProvider("icon", new IconImageProvider());
QuickshellPlugin::runConstructGeneration(*this);
}
EngineGeneration::~EngineGeneration() {

View File

@ -1,8 +1,9 @@
#include "iconimageprovider.hpp"
#include <qicon.h>
#include <qlogging.h>
#include <qpixmap.h>
#include <qsize.h>
#include <qstring.h>
QPixmap
IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {

View File

@ -3,6 +3,8 @@
#include <qvector.h> // NOLINT (what??)
#include "generation.hpp"
static QVector<QuickshellPlugin*> plugins; // NOLINT
void QuickshellPlugin::registerPlugin(QuickshellPlugin& plugin) { plugins.push_back(&plugin); }
@ -26,6 +28,12 @@ void QuickshellPlugin::initPlugins() {
}
}
void QuickshellPlugin::runConstructGeneration(EngineGeneration& generation) {
for (QuickshellPlugin* plugin: plugins) {
plugin->constructGeneration(generation);
}
}
void QuickshellPlugin::runOnReload() {
for (QuickshellPlugin* plugin: plugins) {
plugin->onReload();

View File

@ -3,6 +3,8 @@
#include <qcontainerfwd.h>
#include <qfunctionpointer.h>
class EngineGeneration;
class QuickshellPlugin {
public:
QuickshellPlugin() = default;
@ -15,10 +17,12 @@ public:
virtual bool applies() { return true; }
virtual void init() {}
virtual void registerTypes() {}
virtual void constructGeneration(EngineGeneration& generation) {} // NOLINT
virtual void onReload() {}
static void registerPlugin(QuickshellPlugin& plugin);
static void initPlugins();
static void runConstructGeneration(EngineGeneration& generation);
static void runOnReload();
};

View File

@ -17,6 +17,7 @@
#include <qmetatype.h>
#include <qobject.h>
#include <qpolygon.h>
#include <qtmetamacros.h>
#include <qvariant.h>
#include "dbus_properties.h"
@ -52,11 +53,15 @@ QDBusError demarshallVariant(const QVariant& variant, const QMetaType& type, voi
QDebug(&error) << "failed to deserialize dbus value" << variant << "into" << type;
return QDBusError(QDBusError::InvalidArgs, error);
}
} else {
QString error;
QDebug(&error) << "mismatched signature while trying to demarshall" << variant << "into"
<< type << "expected" << expectedSignature << "got" << signature;
return QDBusError(QDBusError::InvalidArgs, error);
}
} else {
QString error;
QDebug(&error) << "failed to deserialize variant" << variant
<< "which is not a primitive type or a dbus argument (what?)";
QDebug(&error) << "failed to deserialize variant" << variant << "into" << type;
return QDBusError(QDBusError::InvalidArgs, error);
}
@ -226,6 +231,8 @@ void DBusPropertyGroup::updateAllViaGetAll() {
}
delete call;
emit this->getAllFinished();
};
QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback);
@ -262,7 +269,8 @@ void DBusPropertyGroup::onPropertiesChanged(
const QStringList& invalidatedProperties
) {
if (interfaceName != this->interface->interface()) return;
qCDebug(logDbus) << "Received property change set and invalidations for" << this->toString();
qCDebug(logDbus).noquote() << "Received property change set and invalidations for"
<< this->toString();
for (const auto& name: invalidatedProperties) {
auto prop = std::find_if(

View File

@ -122,6 +122,9 @@ public:
void updateAllViaGetAll();
[[nodiscard]] QString toString() const;
signals:
void getAllFinished();
private slots:
void onPropertiesChanged(
const QString& interfaceName,

View File

@ -0,0 +1 @@
add_subdirectory(status_notifier)

View File

@ -0,0 +1,55 @@
qt_add_dbus_adaptor(DBUS_INTERFACES
org.kde.StatusNotifierWatcher.xml
watcher.hpp
qs::service::sni::StatusNotifierWatcher
dbus_watcher
StatusNotifierWatcherAdaptor
)
set_source_files_properties(org.kde.StatusNotifierItem.xml PROPERTIES
CLASSNAME DBusStatusNotifierItem
INCLUDE dbus_item_types.hpp
)
qt_add_dbus_interface(DBUS_INTERFACES
org.kde.StatusNotifierItem.xml
dbus_item
)
set_source_files_properties(org.kde.StatusNotifierWatcher.xml PROPERTIES
CLASSNAME DBusStatusNotifierWatcher
)
qt_add_dbus_interface(DBUS_INTERFACES
org.kde.StatusNotifierWatcher.xml
dbus_watcher_interface
)
qt_add_library(quickshell-service-statusnotifier STATIC
qml.cpp
trayimageprovider.cpp
watcher.cpp
host.cpp
item.cpp
dbus_item_types.cpp
${DBUS_INTERFACES}
)
add_library(quickshell-service-statusnotifier-init OBJECT init.cpp)
# dbus headers
target_include_directories(quickshell-service-statusnotifier PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
qt_add_qml_module(quickshell-service-statusnotifier
URI Quickshell.Services.SystemTray
VERSION 0.1
)
target_link_libraries(quickshell-service-statusnotifier PRIVATE ${QT_DEPS} quickshell-dbus)
target_link_libraries(quickshell-service-statusnotifier-init PRIVATE ${QT_DEPS})
target_link_libraries(quickshell PRIVATE quickshell-service-statusnotifierplugin quickshell-service-statusnotifier-init)
qs_pch(quickshell-service-statusnotifier)
qs_pch(quickshell-service-statusnotifierplugin)
qs_pch(quickshell-service-statusnotifier-init)

View File

@ -0,0 +1,121 @@
#include "dbus_item_types.hpp"
#include <qdbusargument.h>
#include <qdbusextratypes.h>
#include <qdebug.h>
#include <qendian.h>
#include <qimage.h>
#include <qlogging.h>
#include <qmetatype.h>
#include <qsysinfo.h>
#include <qtypes.h>
QImage DBusSniIconPixmap::createImage() const {
// fix byte order if on a little endian machine
if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
auto* newbuf = new quint32[this->data.size()];
const auto* oldbuf = reinterpret_cast<const quint32*>(this->data.data()); // NOLINT
for (uint i = 0; i < this->data.size() / sizeof(quint32); ++i) {
newbuf[i] = qFromBigEndian(oldbuf[i]); // NOLINT
}
return QImage(
reinterpret_cast<const uchar*>(newbuf), // NOLINT
this->width,
this->height,
QImage::Format_ARGB32,
[](void* ptr) { delete reinterpret_cast<quint32*>(ptr); }, // NOLINT
newbuf
);
} else {
return QImage(
reinterpret_cast<const uchar*>(this->data.data()), // NOLINT
this->width,
this->height,
QImage::Format_ARGB32
);
}
}
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusSniIconPixmap& pixmap) {
argument.beginStructure();
argument >> pixmap.width;
argument >> pixmap.height;
argument >> pixmap.data;
argument.endStructure();
return argument;
}
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusSniIconPixmap& pixmap) {
argument.beginStructure();
argument << pixmap.width;
argument << pixmap.height;
argument << pixmap.data;
argument.endStructure();
return argument;
}
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusSniIconPixmapList& pixmaps) {
argument.beginArray();
pixmaps.clear();
while (!argument.atEnd()) {
pixmaps.append(qdbus_cast<DBusSniIconPixmap>(argument));
}
argument.endArray();
return argument;
}
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusSniIconPixmapList& pixmaps) {
argument.beginArray(qMetaTypeId<DBusSniIconPixmap>());
for (const auto& pixmap: pixmaps) {
argument << pixmap;
}
argument.endArray();
return argument;
}
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusSniTooltip& tooltip) {
argument.beginStructure();
argument >> tooltip.icon;
argument >> tooltip.iconPixmaps;
argument >> tooltip.title;
argument >> tooltip.description;
argument.endStructure();
return argument;
}
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusSniTooltip& tooltip) {
argument.beginStructure();
argument << tooltip.icon;
argument << tooltip.iconPixmaps;
argument << tooltip.title;
argument << tooltip.description;
argument.endStructure();
return argument;
}
QDebug operator<<(QDebug debug, const DBusSniIconPixmap& pixmap) {
debug.nospace() << "DBusSniIconPixmap(width=" << pixmap.width << ", height=" << pixmap.height
<< ")";
return debug;
}
QDebug operator<<(QDebug debug, const DBusSniTooltip& tooltip) {
debug.nospace() << "DBusSniTooltip(title=" << tooltip.title
<< ", description=" << tooltip.description << ", icon=" << tooltip.icon
<< ", iconPixmaps=" << tooltip.iconPixmaps << ")";
return debug;
}
QDebug operator<<(QDebug debug, const QDBusObjectPath& path) {
debug.nospace() << "QDBusObjectPath(" << path.path() << ")";
return debug;
}

View File

@ -0,0 +1,35 @@
#pragma once
#include <qdbusargument.h>
#include <qdbusextratypes.h>
#include <qdebug.h>
#include <qlist.h>
struct DBusSniIconPixmap {
qint32 width = 0;
qint32 height = 0;
QByteArray data;
// valid only for the lifetime of the pixmap
[[nodiscard]] QImage createImage() const;
};
using DBusSniIconPixmapList = QList<DBusSniIconPixmap>;
struct DBusSniTooltip {
QString icon;
DBusSniIconPixmapList iconPixmaps;
QString title;
QString description;
};
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusSniIconPixmap& pixmap);
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusSniIconPixmap& pixmap);
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusSniIconPixmapList& pixmaps);
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusSniIconPixmapList& pixmaps);
const QDBusArgument& operator>>(const QDBusArgument& argument, DBusSniTooltip& tooltip);
const QDBusArgument& operator<<(QDBusArgument& argument, const DBusSniTooltip& tooltip);
QDebug operator<<(QDebug debug, const DBusSniIconPixmap& pixmap);
QDebug operator<<(QDebug debug, const DBusSniTooltip& tooltip);
QDebug operator<<(QDebug debug, const QDBusObjectPath& path);

View File

@ -0,0 +1,188 @@
#include "host.hpp"
#include <qcontainerfwd.h>
#include <qdbusconnection.h>
#include <qdbuserror.h>
#include <qdbusservicewatcher.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <unistd.h>
#include "../../dbus/dbusutil.hpp"
#include "dbus_watcher_interface.h"
#include "item.hpp"
#include "watcher.hpp"
Q_LOGGING_CATEGORY(logStatusNotifierHost, "quickshell.service.sni.host", QtWarningMsg);
namespace qs::service::sni {
StatusNotifierHost::StatusNotifierHost(QObject* parent): QObject(parent) {
StatusNotifierWatcher::instance(); // ensure at least one watcher exists
auto bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qCWarning(logStatusNotifierHost)
<< "Could not connect to DBus. StatusNotifier service will not work.";
return;
}
this->hostId = QString("org.kde.StatusNotifierHost-") + QString::number(getpid());
auto success = bus.registerService(this->hostId);
if (!success) {
qCWarning(logStatusNotifierHost) << "Could not register StatusNotifierHost object with DBus. "
"StatusNotifer service will not work.";
return;
}
QObject::connect(
&this->serviceWatcher,
&QDBusServiceWatcher::serviceRegistered,
this,
&StatusNotifierHost::onWatcherRegistered
);
QObject::connect(
&this->serviceWatcher,
&QDBusServiceWatcher::serviceUnregistered,
this,
&StatusNotifierHost::onWatcherUnregistered
);
this->serviceWatcher.addWatchedService("org.kde.StatusNotifierWatcher");
this->serviceWatcher.setConnection(bus);
this->watcher = new DBusStatusNotifierWatcher(
"org.kde.StatusNotifierWatcher",
"/StatusNotifierWatcher",
bus,
this
);
QObject::connect(
this->watcher,
&DBusStatusNotifierWatcher::StatusNotifierItemRegistered,
this,
&StatusNotifierHost::onItemRegistered
);
QObject::connect(
this->watcher,
&DBusStatusNotifierWatcher::StatusNotifierItemUnregistered,
this,
&StatusNotifierHost::onItemUnregistered
);
if (!this->watcher->isValid()) {
qCWarning(logStatusNotifierHost)
<< "Could not find active StatusNotifierWatcher. StatusNotifier service will not work "
"until one is present.";
return;
}
this->connectToWatcher();
}
void StatusNotifierHost::connectToWatcher() {
qCDebug(logStatusNotifierHost) << "Registering host with active StatusNotifierWatcher";
this->watcher->RegisterStatusNotifierHost(this->hostId);
qs::dbus::asyncReadProperty<QStringList>(
*this->watcher,
"RegisteredStatusNotifierItems",
[this](QStringList value, QDBusError error) { // NOLINT
if (error.isValid()) {
qCWarning(logStatusNotifierHost).noquote()
<< "Error reading \"RegisteredStatusNotifierITems\" property of watcher"
<< this->watcher->service();
qCWarning(logStatusNotifierHost) << error;
} else {
qCDebug(logStatusNotifierHost)
<< "Registering preexisting status notifier items from watcher:" << value;
for (auto& item: value) {
this->onItemRegistered(item);
}
}
}
);
}
QList<StatusNotifierItem*> StatusNotifierHost::items() const {
auto items = this->mItems.values();
items.removeIf([](StatusNotifierItem* item) { return !item->isReady(); });
return items;
}
StatusNotifierItem* StatusNotifierHost::itemByService(const QString& service) const {
return this->mItems.value(service);
}
void StatusNotifierHost::onWatcherRegistered() { this->connectToWatcher(); }
void StatusNotifierHost::onWatcherUnregistered() {
qCDebug(logStatusNotifierHost) << "Unregistering StatusNotifierItems from old watcher";
for (auto [service, item]: this->mItems.asKeyValueRange()) {
emit this->itemUnregistered(item);
delete item;
qCDebug(logStatusNotifierHost).noquote()
<< "Unregistered StatusNotifierItem" << service << "from host";
}
this->mItems.clear();
}
void StatusNotifierHost::onItemRegistered(const QString& item) {
if (this->mItems.contains(item)) {
qCDebug(logStatusNotifierHost).noquote()
<< "Ignoring duplicate registration of StatusNotifierItem" << item;
return;
}
qCDebug(logStatusNotifierHost).noquote() << "Registering StatusNotifierItem" << item << "to host";
auto* dItem = new StatusNotifierItem(item, this);
if (!dItem->isValid()) {
qCWarning(logStatusNotifierHost).noquote()
<< "Unable to connect to StatusNotifierItem at" << item;
delete dItem;
return;
}
this->mItems.insert(item, dItem);
QObject::connect(dItem, &StatusNotifierItem::ready, this, &StatusNotifierHost::onItemReady);
emit this->itemRegistered(dItem);
}
void StatusNotifierHost::onItemUnregistered(const QString& item) {
if (auto* dItem = this->mItems.value(item)) {
this->mItems.remove(item);
emit this->itemUnregistered(dItem);
delete dItem;
qCDebug(logStatusNotifierHost).noquote()
<< "Unregistered StatusNotifierItem" << item << "from host";
} else {
qCWarning(logStatusNotifierHost).noquote()
<< "Ignoring unregistration for missing StatusNotifierItem at" << item;
}
}
void StatusNotifierHost::onItemReady() {
if (auto* item = qobject_cast<StatusNotifierItem*>(this->sender())) {
emit this->itemReady(item);
}
}
StatusNotifierHost* StatusNotifierHost::instance() {
static StatusNotifierHost* instance = nullptr; // NOLINT
if (instance == nullptr) instance = new StatusNotifierHost();
return instance;
}
} // namespace qs::service::sni

View File

@ -0,0 +1,49 @@
#pragma once
#include <qcontainerfwd.h>
#include <qdbusservicewatcher.h>
#include <qhash.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include "dbus_watcher_interface.h"
#include "item.hpp"
Q_DECLARE_LOGGING_CATEGORY(logStatusNotifierHost);
namespace qs::service::sni {
class StatusNotifierHost: public QObject {
Q_OBJECT;
public:
explicit StatusNotifierHost(QObject* parent = nullptr);
void connectToWatcher();
[[nodiscard]] QList<StatusNotifierItem*> items() const;
[[nodiscard]] StatusNotifierItem* itemByService(const QString& service) const;
static StatusNotifierHost* instance();
signals:
void itemRegistered(StatusNotifierItem* item);
void itemReady(StatusNotifierItem* item);
void itemUnregistered(StatusNotifierItem* item);
private slots:
void onWatcherRegistered();
void onWatcherUnregistered();
void onItemRegistered(const QString& item);
void onItemUnregistered(const QString& item);
void onItemReady();
private:
QString hostId;
QDBusServiceWatcher serviceWatcher;
DBusStatusNotifierWatcher* watcher = nullptr;
QHash<QString, StatusNotifierItem*> mItems;
};
} // namespace qs::service::sni

View File

@ -0,0 +1,15 @@
#include "../../core/generation.hpp"
#include "../../core/plugin.hpp"
#include "trayimageprovider.hpp"
namespace {
class SniPlugin: public QuickshellPlugin {
void constructGeneration(EngineGeneration& generation) override {
generation.engine.addImageProvider("service.sni", new qs::service::sni::TrayImageProvider());
}
};
QS_REGISTER_PLUGIN(SniPlugin);
} // namespace

View File

@ -0,0 +1,164 @@
#include "item.hpp"
#include <utility>
#include <qdbusmetatype.h>
#include <qdbuspendingcall.h>
#include <qicon.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qpainter.h>
#include <qpixmap.h>
#include <qrect.h>
#include <qsize.h>
#include <qstring.h>
#include <qtmetamacros.h>
#include "../../dbus/dbusutil.hpp"
#include "dbus_item.h"
#include "dbus_item_types.hpp"
#include "host.hpp"
using namespace qs::dbus;
Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg);
namespace qs::service::sni {
StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent): QObject(parent) {
// spec is unclear about what exactly an item address is, so split off anything but the connection path
auto conn = address.split("/").value(0);
this->item =
new DBusStatusNotifierItem(conn, "/StatusNotifierItem", QDBusConnection::sessionBus(), this);
if (!this->item->isValid()) {
qCWarning(logStatusNotifierHost).noquote() << "Cannot create StatusNotifierItem for" << conn;
return;
}
qDBusRegisterMetaType<DBusSniIconPixmap>();
qDBusRegisterMetaType<DBusSniIconPixmapList>();
qDBusRegisterMetaType<DBusSniTooltip>();
// clang-format off
QObject::connect(this->item, &DBusStatusNotifierItem::NewTitle, &this->title, &AbstractDBusProperty::update);
QObject::connect(this->item, &DBusStatusNotifierItem::NewIcon, &this->iconName, &AbstractDBusProperty::update);
QObject::connect(this->item, &DBusStatusNotifierItem::NewIcon, &this->iconPixmaps, &AbstractDBusProperty::update);
//QObject::connect(this->item, &DBusStatusNotifierItem::NewIcon, &this->iconThemePath, &AbstractDBusProperty::update);
QObject::connect(this->item, &DBusStatusNotifierItem::NewOverlayIcon, &this->overlayIconName, &AbstractDBusProperty::update);
QObject::connect(this->item, &DBusStatusNotifierItem::NewOverlayIcon, &this->overlayIconPixmaps, &AbstractDBusProperty::update);
//QObject::connect(this->item, &DBusStatusNotifierItem::NewOverlayIcon, &this->iconThemePath, &AbstractDBusProperty::update);
QObject::connect(this->item, &DBusStatusNotifierItem::NewAttentionIcon, &this->attentionIconName, &AbstractDBusProperty::update);
QObject::connect(this->item, &DBusStatusNotifierItem::NewAttentionIcon, &this->attentionIconPixmaps, &AbstractDBusProperty::update);
//QObject::connect(this->item, &DBusStatusNotifierItem::NewAttentionIcon, &this->iconThemePath, &AbstractDBusProperty::update);
QObject::connect(this->item, &DBusStatusNotifierItem::NewToolTip, &this->tooltip, &AbstractDBusProperty::update);
QObject::connect(&this->iconName, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon);
QObject::connect(&this->attentionIconName, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon);
QObject::connect(&this->overlayIconName, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon);
QObject::connect(&this->iconPixmaps, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon);
QObject::connect(&this->attentionIconPixmaps, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon);
QObject::connect(&this->overlayIconPixmaps, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon);
QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &StatusNotifierItem::onGetAllFinished);
// clang-format on
QObject::connect(this->item, &DBusStatusNotifierItem::NewStatus, this, [this](QString value) {
qCDebug(logStatusNotifierItem) << "Received update for" << this->status.toString() << value;
this->status.set(std::move(value));
});
this->properties.setInterface(this->item);
this->properties.updateAllViaGetAll();
}
bool StatusNotifierItem::isValid() const { return this->item->isValid(); }
bool StatusNotifierItem::isReady() const { return this->mReady; }
QString StatusNotifierItem::iconId() const {
if (this->status.get() == "NeedsAttention") {
auto name = this->attentionIconName.get();
if (!name.isEmpty()) return QString("image://icon/") + name;
} else {
auto name = this->iconName.get();
auto overlayName = this->overlayIconName.get();
if (!name.isEmpty() && overlayName.isEmpty()) return QString("image://icon/") + name;
}
return QString("image://service.sni/") + this->item->service() + "/"
+ QString::number(this->iconIndex);
}
QPixmap StatusNotifierItem::createPixmap(const QSize& size) const {
auto needsAttention = this->status.get() == "NeedsAttention";
auto closestPixmap = [](const QSize& size, const DBusSniIconPixmapList& pixmaps) {
const DBusSniIconPixmap* ret = nullptr;
for (const auto& pixmap: pixmaps) {
if (ret == nullptr) {
ret = &pixmap;
continue;
}
auto existingAdequate = ret->width >= size.width() && ret->height >= size.height();
auto newAdequite = pixmap.width >= size.width() && pixmap.height >= size.height();
auto newSmaller = pixmap.width < ret->width || pixmap.height < ret->height;
if ((existingAdequate && newAdequite && newSmaller) || (!existingAdequate && !newSmaller)) {
ret = &pixmap;
}
}
return ret;
};
QPixmap pixmap;
if (needsAttention) {
if (!this->attentionIconName.get().isEmpty()) {
auto icon = QIcon::fromTheme(this->attentionIconName.get());
pixmap = icon.pixmap(size.width(), size.height());
} else {
const auto* icon = closestPixmap(size, this->attentionIconPixmaps.get());
if (icon != nullptr) pixmap = QPixmap::fromImage(icon->createImage());
}
} else {
if (!this->iconName.get().isEmpty()) {
auto icon = QIcon::fromTheme(this->iconName.get());
pixmap = icon.pixmap(size.width(), size.height());
} else {
const auto* icon = closestPixmap(size, this->iconPixmaps.get());
if (icon != nullptr) pixmap = QPixmap::fromImage(icon->createImage());
}
QPixmap overlay;
if (!this->overlayIconName.get().isEmpty()) {
auto icon = QIcon::fromTheme(this->overlayIconName.get());
overlay = icon.pixmap(pixmap.width(), pixmap.height());
} else {
const auto* icon = closestPixmap(pixmap.size(), this->overlayIconPixmaps.get());
if (icon != nullptr) overlay = QPixmap::fromImage(icon->createImage());
}
if (!overlay.isNull()) {
auto painter = QPainter(&pixmap);
painter.drawPixmap(QRect(0, 0, pixmap.width(), pixmap.height()), overlay);
painter.end();
}
}
return pixmap;
}
void StatusNotifierItem::updateIcon() {
this->iconIndex++;
emit this->iconChanged();
}
void StatusNotifierItem::onGetAllFinished() {
if (this->mReady) return;
this->mReady = true;
emit this->ready();
}
} // namespace qs::service::sni

View File

@ -0,0 +1,66 @@
#pragma once
#include <qdbusextratypes.h>
#include <qdbuspendingcall.h>
#include <qicon.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qpixmap.h>
#include <qtmetamacros.h>
#include "../../dbus/dbusutil.hpp"
#include "dbus_item.h"
#include "dbus_item_types.hpp"
Q_DECLARE_LOGGING_CATEGORY(logStatusNotifierItem);
namespace qs::service::sni {
class StatusNotifierItem: public QObject {
Q_OBJECT;
public:
explicit StatusNotifierItem(const QString& address, QObject* parent = nullptr);
[[nodiscard]] bool isValid() const;
[[nodiscard]] bool isReady() const;
[[nodiscard]] QString iconId() const;
[[nodiscard]] QPixmap createPixmap(const QSize& size) const;
// clang-format off
dbus::DBusPropertyGroup properties;
dbus::DBusProperty<QString> id {this->properties, "Id"};
dbus::DBusProperty<QString> title {this->properties, "Title"};
dbus::DBusProperty<QString> status {this->properties, "Status"};
dbus::DBusProperty<QString> category {this->properties, "Category"};
dbus::DBusProperty<quint32> windowId {this->properties, "WindowId"};
//dbus::DBusProperty<QString> iconThemePath {this->properties, "IconThemePath"};
dbus::DBusProperty<QString> iconName {this->properties, "IconName"};
dbus::DBusProperty<DBusSniIconPixmapList> iconPixmaps {this->properties, "IconPixmap"};
dbus::DBusProperty<QString> overlayIconName {this->properties, "OverlayIconName"};
dbus::DBusProperty<DBusSniIconPixmapList> overlayIconPixmaps {this->properties, "OverlayIconPixmap"};
dbus::DBusProperty<QString> attentionIconName {this->properties, "AttentionIconName"};
dbus::DBusProperty<DBusSniIconPixmapList> attentionIconPixmaps {this->properties, "AttentionIconPixmap"};
dbus::DBusProperty<QString> attentionMovieName {this->properties, "AttentionMovieName"};
dbus::DBusProperty<DBusSniTooltip> tooltip {this->properties, "ToolTip"};
dbus::DBusProperty<bool> isMenu {this->properties, "ItemIsMenu"};
dbus::DBusProperty<QDBusObjectPath> menuPath {this->properties, "Menu"};
// clang-format on
signals:
void iconChanged();
void ready();
private slots:
void updateIcon();
void onGetAllFinished();
private:
DBusStatusNotifierItem* item = nullptr;
bool mReady = false;
// bumped to inhibit caching
quint32 iconIndex = 0;
};
} // namespace qs::service::sni

View File

@ -0,0 +1,54 @@
<node>
<interface name="org.kde.StatusNotifierItem">
<property name="Category" type="s" access="read"/>
<property name="Id" type="s" access="read"/>
<property name="Title" type="s" access="read"/>
<property name="Status" type="s" access="read"/>
<property name="WindowId" type="u" access="read"/>
<property name="IconThemePath" type="s" access="read"/>
<property name="IconName" type="s" access="read"/>
<property name="IconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="DBusSniIconPixmapList"/>
</property>
<property name="OverlayIconName" type="s" access="read"/>
<property name="OverlayIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="DBusSniIconPixmapList"/>
</property>
<property name="AttentionIconName" type="s" access="read"/>
<property name="AttentionIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="DBusSniIconPixmapList"/>
</property>
<property name="AttentionMovieName" type="s" access="read"/>
<property name="ToolTip" type="(sa(iiay)ss)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="DBusSniTooltip"/>
</property>
<property name="Menu" type="o" access="read"/>
<property name="ItemIsMenu" type="b" access="read"/>
<method name="ContextMenu">
<arg type="i" direction="in" name="x"/>
<arg type="i" direction="in" name="y"/>
</method>
<method name="Activate">
<arg type="i" direction="in" name="x"/>
<arg type="i" direction="in" name="y"/>
</method>
<method name="SecondaryActivate">
<arg type="i" direction="in" name="x"/>
<arg type="i" direction="in" name="y"/>
</method>
<method name="Scroll">
<arg type="i" direction="in" name="delta"/>
<arg type="s" direction="in" name="orientation"/>
</method>
<signal name="NewTitle"/>
<signal name="NewIcon"/>
<signal name="NewAttentionIcon"/>
<signal name="NewOverlayIcon"/>
<signal name="NewToolTip"/>
<signal name="NewStatus">
<arg type="s" name="status"/>
</signal>
</interface>
</node>

View File

@ -0,0 +1,23 @@
<node>
<interface name="org.kde.StatusNotifierWatcher">
<property name="ProtocolVersion" type="i" access="read"/>
<property name="RegisteredStatusNotifierItems" type="as" access="read"/>
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
<method name="RegisterStatusNotifierItem">
<arg name="service" type="s" direction="in"/>
</method>
<method name="RegisterStatusNotifierHost">
<arg name="service" type="s" direction="in"/>
</method>
<signal name="StatusNotifierItemRegistered">
<arg type="s" direction="out" name="service"/>
</signal>
<signal name="StatusNotifierItemUnregistered">
<arg type="s" direction="out" name="service"/>
</signal>
<signal name="StatusNotifierHostRegistered"/>
<signal name="StatusNotifierHostUnregistered"/>
</interface>
</node>

View File

@ -0,0 +1,140 @@
#include "qml.hpp"
#include <qcontainerfwd.h>
#include <qdebug.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "../../dbus/dbusutil.hpp"
#include "host.hpp"
#include "item.hpp"
using namespace qs::dbus;
using namespace qs::service::sni;
SystemTrayItem::SystemTrayItem(qs::service::sni::StatusNotifierItem* item, QObject* parent)
: QObject(parent)
, item(item) {
// clang-format off
QObject::connect(&this->item->id, &AbstractDBusProperty::changed, this, &SystemTrayItem::idChanged);
QObject::connect(&this->item->title, &AbstractDBusProperty::changed, this, &SystemTrayItem::titleChanged);
QObject::connect(&this->item->status, &AbstractDBusProperty::changed, this, &SystemTrayItem::statusChanged);
QObject::connect(&this->item->category, &AbstractDBusProperty::changed, this, &SystemTrayItem::categoryChanged);
QObject::connect(this->item, &StatusNotifierItem::iconChanged, this, &SystemTrayItem::iconChanged);
QObject::connect(&this->item->tooltip, &AbstractDBusProperty::changed, this, &SystemTrayItem::tooltipTitleChanged);
QObject::connect(&this->item->tooltip, &AbstractDBusProperty::changed, this, &SystemTrayItem::tooltipDescriptionChanged);
QObject::connect(&this->item->isMenu, &AbstractDBusProperty::changed, this, &SystemTrayItem::onlyMenuChanged);
// clang-format on
}
QString SystemTrayItem::id() const {
if (this->item == nullptr) return "";
return this->item->id.get();
}
QString SystemTrayItem::title() const {
if (this->item == nullptr) return "";
return this->item->title.get();
}
SystemTrayStatus::Enum SystemTrayItem::status() const {
if (this->item == nullptr) return SystemTrayStatus::Passive;
auto status = this->item->status.get();
if (status == "Passive") return SystemTrayStatus::Passive;
if (status == "Active") return SystemTrayStatus::Active;
if (status == "NeedsAttention") return SystemTrayStatus::NeedsAttention;
qCWarning(logStatusNotifierItem) << "Nonconformant StatusNotifierItem status" << status
<< "returned for" << this->item->properties.toString();
return SystemTrayStatus::Passive;
}
SystemTrayCategory::Enum SystemTrayItem::category() const {
if (this->item == nullptr) return SystemTrayCategory::ApplicationStatus;
auto category = this->item->category.get();
if (category == "ApplicationStatus") return SystemTrayCategory::ApplicationStatus;
if (category == "SystemServices") return SystemTrayCategory::SystemServices;
if (category == "Hardware") return SystemTrayCategory::Hardware;
qCWarning(logStatusNotifierItem) << "Nonconformant StatusNotifierItem category" << category
<< "returned for" << this->item->properties.toString();
return SystemTrayCategory::ApplicationStatus;
}
QString SystemTrayItem::icon() const {
if (this->item == nullptr) return "";
return this->item->iconId();
}
QString SystemTrayItem::tooltipTitle() const {
if (this->item == nullptr) return "";
return this->item->tooltip.get().title;
}
QString SystemTrayItem::tooltipDescription() const {
if (this->item == nullptr) return "";
return this->item->tooltip.get().description;
}
bool SystemTrayItem::onlyMenu() const {
if (this->item == nullptr) return false;
return this->item->isMenu.get();
}
SystemTray::SystemTray(QObject* parent): QObject(parent) {
auto* host = StatusNotifierHost::instance();
// clang-format off
QObject::connect(host, &StatusNotifierHost::itemReady, this, &SystemTray::onItemRegistered);
QObject::connect(host, &StatusNotifierHost::itemUnregistered, this, &SystemTray::onItemUnregistered);
// clang-format on
for (auto* item: host->items()) {
this->mItems.push_back(new SystemTrayItem(item, this));
}
}
void SystemTray::onItemRegistered(StatusNotifierItem* item) {
this->mItems.push_back(new SystemTrayItem(item, this));
emit this->itemsChanged();
}
void SystemTray::onItemUnregistered(StatusNotifierItem* item) {
SystemTrayItem* trayItem = nullptr;
this->mItems.removeIf([item, &trayItem](SystemTrayItem* testItem) {
if (testItem->item == item) {
trayItem = testItem;
return true;
} else return false;
});
emit this->itemsChanged();
delete trayItem;
}
QQmlListProperty<SystemTrayItem> SystemTray::items() {
return QQmlListProperty<SystemTrayItem>(
this,
nullptr,
&SystemTray::itemsCount,
&SystemTray::itemAt
);
}
qsizetype SystemTray::itemsCount(QQmlListProperty<SystemTrayItem>* property) {
return reinterpret_cast<SystemTray*>(property->object)->mItems.count(); // NOLINT
}
SystemTrayItem* SystemTray::itemAt(QQmlListProperty<SystemTrayItem>* property, qsizetype index) {
return reinterpret_cast<SystemTray*>(property->object)->mItems.at(index); // NOLINT
}

View File

@ -0,0 +1,128 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include "item.hpp"
namespace SystemTrayStatus { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
// A passive item does not convey important information and can be considered idle. You may want to hide these.
Passive = 0,
// An active item may have information more important than a passive one and you probably do not want to hide it.
Active = 1,
// An item that needs attention conveys very important information such as low battery.
NeedsAttention = 2,
};
Q_ENUM_NS(Enum);
} // namespace SystemTrayStatus
namespace SystemTrayCategory { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
// The fallback category for general applications or anything that does
// not fit into a different category.
ApplicationStatus = 0,
// System services such as IMEs or disk indexing.
SystemServices = 1,
// Hardware controls like battery indicators or volume control.
Hardware = 2,
};
Q_ENUM_NS(Enum);
} // namespace SystemTrayCategory
///! An item in the system tray.
/// A system tray item, roughly conforming to the [kde/freedesktop spec]
/// (there is no real spec, we just implemented whatever seemed to actually be used).
///
/// [kde/freedesktop spec]: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/
class SystemTrayItem: public QObject {
Q_OBJECT;
// A name unique to the application, such as its name.
Q_PROPERTY(QString id READ id NOTIFY idChanged);
// A name that describes the application
Q_PROPERTY(QString title READ title NOTIFY titleChanged);
Q_PROPERTY(SystemTrayStatus::Enum status READ status NOTIFY statusChanged);
Q_PROPERTY(SystemTrayCategory::Enum category READ category NOTIFY categoryChanged);
// Icon source string, usable as an Image source.
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged);
Q_PROPERTY(QString tooltipTitle READ tooltipTitle NOTIFY tooltipTitleChanged);
Q_PROPERTY(QString tooltipDescription READ tooltipDescription NOTIFY tooltipDescriptionChanged);
// If this tray item only offers a menu and no activation action.
Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged);
QML_ELEMENT;
public:
explicit SystemTrayItem(
qs::service::sni::StatusNotifierItem* item = nullptr,
QObject* parent = nullptr
);
// Primary activation action, generally triggered via a left click.
//Q_INVOKABLE void activate();
// Secondary activation action, generally triggered via a middle click.
//Q_INVOKABLE void secondaryActivate();
// Scroll action, such as changing volume on a mixer.
//Q_INVOKABLE void scroll(qint32 delta, bool horizontal);
[[nodiscard]] QString id() const;
[[nodiscard]] QString title() const;
[[nodiscard]] SystemTrayStatus::Enum status() const;
[[nodiscard]] SystemTrayCategory::Enum category() const;
[[nodiscard]] QString icon() const;
[[nodiscard]] QString tooltipTitle() const;
[[nodiscard]] QString tooltipDescription() const;
[[nodiscard]] bool onlyMenu() const;
signals:
void idChanged();
void titleChanged();
void statusChanged();
void categoryChanged();
void iconChanged();
void tooltipTitleChanged();
void tooltipDescriptionChanged();
void onlyMenuChanged();
private:
qs::service::sni::StatusNotifierItem* item = nullptr;
friend class SystemTray;
};
class SystemTray: public QObject {
Q_OBJECT;
Q_PROPERTY(QQmlListProperty<SystemTrayItem> items READ items NOTIFY itemsChanged);
QML_ELEMENT;
QML_SINGLETON;
public:
explicit SystemTray(QObject* parent = nullptr);
[[nodiscard]] QQmlListProperty<SystemTrayItem> items();
signals:
void itemsChanged();
private slots:
void onItemRegistered(qs::service::sni::StatusNotifierItem* item);
void onItemUnregistered(qs::service::sni::StatusNotifierItem* item);
private:
static qsizetype itemsCount(QQmlListProperty<SystemTrayItem>* property);
static SystemTrayItem* itemAt(QQmlListProperty<SystemTrayItem>* property, qsizetype index);
QList<SystemTrayItem*> mItems;
};

View File

@ -0,0 +1,33 @@
#include "trayimageprovider.hpp"
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qpixmap.h>
#include <qsize.h>
#include <qstring.h>
#include "host.hpp"
namespace qs::service::sni {
QPixmap
TrayImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
auto split = id.split('/');
if (split.size() != 2) {
qCWarning(logStatusNotifierHost) << "Invalid image request:" << id;
return QPixmap();
}
auto* item = StatusNotifierHost::instance()->itemByService(split[0]);
if (item == nullptr) {
qCWarning(logStatusNotifierHost) << "Image requested for nonexistant service" << split[0];
return QPixmap();
}
auto pixmap = item->createPixmap(requestedSize);
if (size != nullptr) *size = pixmap.size();
return pixmap;
}
} // namespace qs::service::sni

View File

@ -0,0 +1,16 @@
#pragma once
#include <qpixmap.h>
#include <qquickimageprovider.h>
#include <qtmetamacros.h>
namespace qs::service::sni {
class TrayImageProvider: public QQuickImageProvider {
public:
explicit TrayImageProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {}
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
};
} // namespace qs::service::sni

View File

@ -0,0 +1,140 @@
#include "watcher.hpp"
#include <dbus_watcher.h>
#include <qdbusconnection.h>
#include <qdbusconnectioninterface.h>
#include <qdbusservicewatcher.h>
#include <qlist.h>
#include <qlogging.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
Q_LOGGING_CATEGORY(logStatusNotifierWatcher, "quickshell.service.sni.watcher", QtWarningMsg);
namespace qs::service::sni {
StatusNotifierWatcher::StatusNotifierWatcher(QObject* parent): QObject(parent) {
new StatusNotifierWatcherAdaptor(this);
qCDebug(logStatusNotifierWatcher) << "Starting StatusNotifierWatcher";
auto bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qCWarning(logStatusNotifierWatcher)
<< "Could not connect to DBus. StatusNotifier service will not work.";
return;
}
if (!bus.registerObject("/StatusNotifierWatcher", this)) {
qCWarning(logStatusNotifierWatcher) << "Could not register StatusNotifierWatcher object with "
"DBus. StatusNotifer service will not work.";
return;
}
QObject::connect(
&this->serviceWatcher,
&QDBusServiceWatcher::serviceUnregistered,
this,
&StatusNotifierWatcher::onServiceUnregistered
);
this->serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
this->serviceWatcher.addWatchedService("org.kde.StatusNotifierWatcher");
this->serviceWatcher.setConnection(bus);
this->tryRegister();
}
void StatusNotifierWatcher::tryRegister() { // NOLINT
auto bus = QDBusConnection::sessionBus();
auto success = bus.registerService("org.kde.StatusNotifierWatcher");
if (success) {
qCDebug(logStatusNotifierWatcher) << "Registered watcher at org.kde.StatusNotifierWatcher";
} else {
qCDebug(logStatusNotifierWatcher)
<< "Could not register watcher at org.kde.StatusNotifierWatcher, presumably because one is "
"already registered.";
qCDebug(logStatusNotifierWatcher)
<< "Registration will be attempted again if the active service is unregistered.";
}
}
void StatusNotifierWatcher::onServiceUnregistered(const QString& service) {
if (service == "org.kde.StatusNotifierWatcher") {
qCDebug(logStatusNotifierWatcher)
<< "Active StatusNotifierWatcher unregistered, attempting registration";
this->tryRegister();
return;
} else if (this->items.removeAll(service) != 0) {
qCDebug(logStatusNotifierWatcher).noquote()
<< "Unregistered StatusNotifierItem" << service << "from watcher";
emit this->StatusNotifierItemUnregistered(service);
} else if (this->hosts.removeAll(service) != 0) {
qCDebug(logStatusNotifierWatcher).noquote()
<< "Unregistered StatusNotifierHost" << service << "from watcher";
emit this->StatusNotifierHostUnregistered();
} else {
qCWarning(logStatusNotifierWatcher).noquote()
<< "Got service unregister event for untracked service" << service;
}
this->serviceWatcher.removeWatchedService(service);
}
bool StatusNotifierWatcher::isHostRegistered() const { // NOLINT
// no point ever returning false
return true;
}
QList<QString> StatusNotifierWatcher::registeredItems() const { return this->items; }
void StatusNotifierWatcher::RegisterStatusNotifierHost(const QString& host) {
if (this->hosts.contains(host)) {
qCDebug(logStatusNotifierWatcher).noquote()
<< "Skipping duplicate registration of StatusNotifierHost" << host << "to watcher";
return;
}
if (!QDBusConnection::sessionBus().interface()->serviceOwner(host).isValid()) {
qCWarning(logStatusNotifierWatcher).noquote()
<< "Ignoring invalid StatusNotifierHost registration of" << host << "to watcher";
return;
}
this->serviceWatcher.addWatchedService(host);
this->hosts.push_back(host);
qCDebug(logStatusNotifierWatcher).noquote()
<< "Registered StatusNotifierHost" << host << "to watcher";
emit this->StatusNotifierHostRegistered();
}
void StatusNotifierWatcher::RegisterStatusNotifierItem(const QString& item) {
if (this->items.contains(item)) {
qCDebug(logStatusNotifierWatcher).noquote()
<< "Skipping duplicate registration of StatusNotifierItem" << item << "to watcher";
return;
}
if (!QDBusConnection::sessionBus().interface()->serviceOwner(item).isValid()) {
qCWarning(logStatusNotifierWatcher).noquote()
<< "Ignoring invalid StatusNotifierItem registration of" << item << "to watcher";
return;
}
this->serviceWatcher.addWatchedService(item);
this->items.push_back(item);
qCDebug(logStatusNotifierWatcher).noquote()
<< "Registered StatusNotifierItem" << item << "to watcher";
emit this->StatusNotifierItemRegistered(item);
}
StatusNotifierWatcher* StatusNotifierWatcher::instance() {
static StatusNotifierWatcher* instance = nullptr; // NOLINT
if (instance == nullptr) instance = new StatusNotifierWatcher();
return instance;
}
} // namespace qs::service::sni

View File

@ -0,0 +1,54 @@
#pragma once
#include <qdbusinterface.h>
#include <qdbusservicewatcher.h>
#include <qlist.h>
#include <qloggingcategory.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qtypes.h>
Q_DECLARE_LOGGING_CATEGORY(logStatusNotifierWatcher);
namespace qs::service::sni {
class StatusNotifierWatcher: public QObject {
Q_OBJECT;
Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion);
Q_PROPERTY(bool IsStatusNotifierHostRegistered READ isHostRegistered);
Q_PROPERTY(QList<QString> RegisteredStatusNotifierItems READ registeredItems);
public:
explicit StatusNotifierWatcher(QObject* parent = nullptr);
void tryRegister();
[[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT
[[nodiscard]] bool isHostRegistered() const;
[[nodiscard]] QList<QString> registeredItems() const;
// NOLINTBEGIN
void RegisterStatusNotifierHost(const QString& host);
void RegisterStatusNotifierItem(const QString& item);
// NOLINTEND
static StatusNotifierWatcher* instance();
signals:
// NOLINTBEGIN
void StatusNotifierHostRegistered();
void StatusNotifierHostUnregistered();
void StatusNotifierItemRegistered(const QString& service);
void StatusNotifierItemUnregistered(const QString& service);
// NOLINTEND
private slots:
void onServiceUnregistered(const QString& service);
private:
QDBusServiceWatcher serviceWatcher;
QList<QString> hosts;
QList<QString> items;
};
} // namespace qs::service::sni