forked from quickshell/quickshell
service/tray: mostly complete StatusNotifierItem implementation
Notably missing dbusmenu which makes it actually useful.
This commit is contained in:
parent
d47a7f2cff
commit
6214ac1002
|
@ -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)
|
||||
|
|
|
@ -12,3 +12,5 @@ endif()
|
|||
if (WAYLAND)
|
||||
add_subdirectory(wayland)
|
||||
endif ()
|
||||
|
||||
add_subdirectory(services)
|
||||
|
|
|
@ -33,6 +33,8 @@ EngineGeneration::EngineGeneration(QmlScanner scanner)
|
|||
this->engine.setIncubationController(&this->delayedIncubationController);
|
||||
|
||||
this->engine.addImageProvider("icon", new IconImageProvider());
|
||||
|
||||
QuickshellPlugin::runConstructGeneration(*this);
|
||||
}
|
||||
|
||||
EngineGeneration::~EngineGeneration() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -122,6 +122,9 @@ public:
|
|||
void updateAllViaGetAll();
|
||||
[[nodiscard]] QString toString() const;
|
||||
|
||||
signals:
|
||||
void getAllFinished();
|
||||
|
||||
private slots:
|
||||
void onPropertiesChanged(
|
||||
const QString& interfaceName,
|
||||
|
|
1
src/services/CMakeLists.txt
Normal file
1
src/services/CMakeLists.txt
Normal file
|
@ -0,0 +1 @@
|
|||
add_subdirectory(status_notifier)
|
55
src/services/status_notifier/CMakeLists.txt
Normal file
55
src/services/status_notifier/CMakeLists.txt
Normal 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)
|
121
src/services/status_notifier/dbus_item_types.cpp
Normal file
121
src/services/status_notifier/dbus_item_types.cpp
Normal 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;
|
||||
}
|
35
src/services/status_notifier/dbus_item_types.hpp
Normal file
35
src/services/status_notifier/dbus_item_types.hpp
Normal 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);
|
188
src/services/status_notifier/host.cpp
Normal file
188
src/services/status_notifier/host.cpp
Normal 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
|
49
src/services/status_notifier/host.hpp
Normal file
49
src/services/status_notifier/host.hpp
Normal 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
|
15
src/services/status_notifier/init.cpp
Normal file
15
src/services/status_notifier/init.cpp
Normal 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
|
164
src/services/status_notifier/item.cpp
Normal file
164
src/services/status_notifier/item.cpp
Normal 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
|
66
src/services/status_notifier/item.hpp
Normal file
66
src/services/status_notifier/item.hpp
Normal 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
|
54
src/services/status_notifier/org.kde.StatusNotifierItem.xml
Normal file
54
src/services/status_notifier/org.kde.StatusNotifierItem.xml
Normal 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>
|
|
@ -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>
|
140
src/services/status_notifier/qml.cpp
Normal file
140
src/services/status_notifier/qml.cpp
Normal 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
|
||||
}
|
128
src/services/status_notifier/qml.hpp
Normal file
128
src/services/status_notifier/qml.hpp
Normal 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;
|
||||
};
|
33
src/services/status_notifier/trayimageprovider.cpp
Normal file
33
src/services/status_notifier/trayimageprovider.cpp
Normal 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
|
16
src/services/status_notifier/trayimageprovider.hpp
Normal file
16
src/services/status_notifier/trayimageprovider.hpp
Normal 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
|
140
src/services/status_notifier/watcher.cpp
Normal file
140
src/services/status_notifier/watcher.cpp
Normal 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
|
54
src/services/status_notifier/watcher.hpp
Normal file
54
src/services/status_notifier/watcher.hpp
Normal 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
|
Loading…
Reference in a new issue