From 6214ac10029cabeed1a69ed466040b99949861a0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 6 Apr 2024 02:19:40 -0700 Subject: [PATCH] service/tray: mostly complete StatusNotifierItem implementation Notably missing dbusmenu which makes it actually useful. --- CMakeLists.txt | 7 + src/CMakeLists.txt | 2 + src/core/generation.cpp | 2 + src/core/iconimageprovider.cpp | 3 +- src/core/plugin.cpp | 8 + src/core/plugin.hpp | 4 + src/dbus/dbusutil.cpp | 14 +- src/dbus/dbusutil.hpp | 3 + src/services/CMakeLists.txt | 1 + src/services/status_notifier/CMakeLists.txt | 55 +++++ .../status_notifier/dbus_item_types.cpp | 121 +++++++++++ .../status_notifier/dbus_item_types.hpp | 35 ++++ src/services/status_notifier/host.cpp | 188 ++++++++++++++++++ src/services/status_notifier/host.hpp | 49 +++++ src/services/status_notifier/init.cpp | 15 ++ src/services/status_notifier/item.cpp | 164 +++++++++++++++ src/services/status_notifier/item.hpp | 66 ++++++ .../org.kde.StatusNotifierItem.xml | 54 +++++ .../org.kde.StatusNotifierWatcher.xml | 23 +++ src/services/status_notifier/qml.cpp | 140 +++++++++++++ src/services/status_notifier/qml.hpp | 128 ++++++++++++ .../status_notifier/trayimageprovider.cpp | 33 +++ .../status_notifier/trayimageprovider.hpp | 16 ++ src/services/status_notifier/watcher.cpp | 140 +++++++++++++ src/services/status_notifier/watcher.hpp | 54 +++++ 25 files changed, 1321 insertions(+), 4 deletions(-) create mode 100644 src/services/CMakeLists.txt create mode 100644 src/services/status_notifier/CMakeLists.txt create mode 100644 src/services/status_notifier/dbus_item_types.cpp create mode 100644 src/services/status_notifier/dbus_item_types.hpp create mode 100644 src/services/status_notifier/host.cpp create mode 100644 src/services/status_notifier/host.hpp create mode 100644 src/services/status_notifier/init.cpp create mode 100644 src/services/status_notifier/item.cpp create mode 100644 src/services/status_notifier/item.hpp create mode 100644 src/services/status_notifier/org.kde.StatusNotifierItem.xml create mode 100644 src/services/status_notifier/org.kde.StatusNotifierWatcher.xml create mode 100644 src/services/status_notifier/qml.cpp create mode 100644 src/services/status_notifier/qml.hpp create mode 100644 src/services/status_notifier/trayimageprovider.cpp create mode 100644 src/services/status_notifier/trayimageprovider.hpp create mode 100644 src/services/status_notifier/watcher.cpp create mode 100644 src/services/status_notifier/watcher.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c6a8a6d..eb68ab12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 090fbc25..8fe9c651 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,3 +12,5 @@ endif() if (WAYLAND) add_subdirectory(wayland) endif () + +add_subdirectory(services) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index a925ec84..38464ed5 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -33,6 +33,8 @@ EngineGeneration::EngineGeneration(QmlScanner scanner) this->engine.setIncubationController(&this->delayedIncubationController); this->engine.addImageProvider("icon", new IconImageProvider()); + + QuickshellPlugin::runConstructGeneration(*this); } EngineGeneration::~EngineGeneration() { diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp index 45aa9569..762600ba 100644 --- a/src/core/iconimageprovider.cpp +++ b/src/core/iconimageprovider.cpp @@ -1,8 +1,9 @@ #include "iconimageprovider.hpp" #include -#include #include +#include +#include QPixmap IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) { diff --git a/src/core/plugin.cpp b/src/core/plugin.cpp index 21312de1..8f1d0e96 100644 --- a/src/core/plugin.cpp +++ b/src/core/plugin.cpp @@ -3,6 +3,8 @@ #include // NOLINT (what??) +#include "generation.hpp" + static QVector 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(); diff --git a/src/core/plugin.hpp b/src/core/plugin.hpp index 8a3719b1..8e168241 100644 --- a/src/core/plugin.hpp +++ b/src/core/plugin.hpp @@ -3,6 +3,8 @@ #include #include +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(); }; diff --git a/src/dbus/dbusutil.cpp b/src/dbus/dbusutil.cpp index a80d6223..26860b1e 100644 --- a/src/dbus/dbusutil.cpp +++ b/src/dbus/dbusutil.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #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( diff --git a/src/dbus/dbusutil.hpp b/src/dbus/dbusutil.hpp index 1c5f3f04..480a6c22 100644 --- a/src/dbus/dbusutil.hpp +++ b/src/dbus/dbusutil.hpp @@ -122,6 +122,9 @@ public: void updateAllViaGetAll(); [[nodiscard]] QString toString() const; +signals: + void getAllFinished(); + private slots: void onPropertiesChanged( const QString& interfaceName, diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt new file mode 100644 index 00000000..909acc00 --- /dev/null +++ b/src/services/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(status_notifier) diff --git a/src/services/status_notifier/CMakeLists.txt b/src/services/status_notifier/CMakeLists.txt new file mode 100644 index 00000000..689c347d --- /dev/null +++ b/src/services/status_notifier/CMakeLists.txt @@ -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) diff --git a/src/services/status_notifier/dbus_item_types.cpp b/src/services/status_notifier/dbus_item_types.cpp new file mode 100644 index 00000000..567f4644 --- /dev/null +++ b/src/services/status_notifier/dbus_item_types.cpp @@ -0,0 +1,121 @@ +#include "dbus_item_types.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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(newbuf), // NOLINT + this->width, + this->height, + QImage::Format_ARGB32, + [](void* ptr) { delete reinterpret_cast(ptr); }, // NOLINT + newbuf + ); + } else { + return QImage( + reinterpret_cast(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(argument)); + } + + argument.endArray(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusSniIconPixmapList& pixmaps) { + argument.beginArray(qMetaTypeId()); + + 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; +} diff --git a/src/services/status_notifier/dbus_item_types.hpp b/src/services/status_notifier/dbus_item_types.hpp new file mode 100644 index 00000000..13c1a94b --- /dev/null +++ b/src/services/status_notifier/dbus_item_types.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +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; + +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); diff --git a/src/services/status_notifier/host.cpp b/src/services/status_notifier/host.cpp new file mode 100644 index 00000000..84ef3106 --- /dev/null +++ b/src/services/status_notifier/host.cpp @@ -0,0 +1,188 @@ +#include "host.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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( + *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 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(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 diff --git a/src/services/status_notifier/host.hpp b/src/services/status_notifier/host.hpp new file mode 100644 index 00000000..9d823e5f --- /dev/null +++ b/src/services/status_notifier/host.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#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 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 mItems; +}; + +} // namespace qs::service::sni diff --git a/src/services/status_notifier/init.cpp b/src/services/status_notifier/init.cpp new file mode 100644 index 00000000..58a49fae --- /dev/null +++ b/src/services/status_notifier/init.cpp @@ -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 diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp new file mode 100644 index 00000000..fa9c713a --- /dev/null +++ b/src/services/status_notifier/item.cpp @@ -0,0 +1,164 @@ +#include "item.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + // 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 diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp new file mode 100644 index 00000000..89c65537 --- /dev/null +++ b/src/services/status_notifier/item.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#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 id {this->properties, "Id"}; + dbus::DBusProperty title {this->properties, "Title"}; + dbus::DBusProperty status {this->properties, "Status"}; + dbus::DBusProperty category {this->properties, "Category"}; + dbus::DBusProperty windowId {this->properties, "WindowId"}; + //dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; + dbus::DBusProperty iconName {this->properties, "IconName"}; + dbus::DBusProperty iconPixmaps {this->properties, "IconPixmap"}; + dbus::DBusProperty overlayIconName {this->properties, "OverlayIconName"}; + dbus::DBusProperty overlayIconPixmaps {this->properties, "OverlayIconPixmap"}; + dbus::DBusProperty attentionIconName {this->properties, "AttentionIconName"}; + dbus::DBusProperty attentionIconPixmaps {this->properties, "AttentionIconPixmap"}; + dbus::DBusProperty attentionMovieName {this->properties, "AttentionMovieName"}; + dbus::DBusProperty tooltip {this->properties, "ToolTip"}; + dbus::DBusProperty isMenu {this->properties, "ItemIsMenu"}; + dbus::DBusProperty 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 diff --git a/src/services/status_notifier/org.kde.StatusNotifierItem.xml b/src/services/status_notifier/org.kde.StatusNotifierItem.xml new file mode 100644 index 00000000..0aa43c7c --- /dev/null +++ b/src/services/status_notifier/org.kde.StatusNotifierItem.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/services/status_notifier/org.kde.StatusNotifierWatcher.xml b/src/services/status_notifier/org.kde.StatusNotifierWatcher.xml new file mode 100644 index 00000000..5d5f3270 --- /dev/null +++ b/src/services/status_notifier/org.kde.StatusNotifierWatcher.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp new file mode 100644 index 00000000..c5858fec --- /dev/null +++ b/src/services/status_notifier/qml.cpp @@ -0,0 +1,140 @@ +#include "qml.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 SystemTray::items() { + return QQmlListProperty( + this, + nullptr, + &SystemTray::itemsCount, + &SystemTray::itemAt + ); +} + +qsizetype SystemTray::itemsCount(QQmlListProperty* property) { + return reinterpret_cast(property->object)->mItems.count(); // NOLINT +} + +SystemTrayItem* SystemTray::itemAt(QQmlListProperty* property, qsizetype index) { + return reinterpret_cast(property->object)->mItems.at(index); // NOLINT +} diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp new file mode 100644 index 00000000..ad7e8ceb --- /dev/null +++ b/src/services/status_notifier/qml.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include +#include +#include +#include +#include + +#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 items READ items NOTIFY itemsChanged); + QML_ELEMENT; + QML_SINGLETON; + +public: + explicit SystemTray(QObject* parent = nullptr); + + [[nodiscard]] QQmlListProperty 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* property); + static SystemTrayItem* itemAt(QQmlListProperty* property, qsizetype index); + + QList mItems; +}; diff --git a/src/services/status_notifier/trayimageprovider.cpp b/src/services/status_notifier/trayimageprovider.cpp new file mode 100644 index 00000000..cca89a97 --- /dev/null +++ b/src/services/status_notifier/trayimageprovider.cpp @@ -0,0 +1,33 @@ +#include "trayimageprovider.hpp" + +#include +#include +#include +#include +#include + +#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 diff --git a/src/services/status_notifier/trayimageprovider.hpp b/src/services/status_notifier/trayimageprovider.hpp new file mode 100644 index 00000000..a4b245b0 --- /dev/null +++ b/src/services/status_notifier/trayimageprovider.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +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 diff --git a/src/services/status_notifier/watcher.cpp b/src/services/status_notifier/watcher.cpp new file mode 100644 index 00000000..c5a665d4 --- /dev/null +++ b/src/services/status_notifier/watcher.cpp @@ -0,0 +1,140 @@ +#include "watcher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 diff --git a/src/services/status_notifier/watcher.hpp b/src/services/status_notifier/watcher.hpp new file mode 100644 index 00000000..f881d716 --- /dev/null +++ b/src/services/status_notifier/watcher.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +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 RegisteredStatusNotifierItems READ registeredItems); + +public: + explicit StatusNotifierWatcher(QObject* parent = nullptr); + + void tryRegister(); + + [[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT + [[nodiscard]] bool isHostRegistered() const; + [[nodiscard]] QList 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 hosts; + QList items; +}; + +} // namespace qs::service::sni