service/tray: account for more edge cases and add placeholder img

This commit is contained in:
outfoxxed 2024-04-19 04:12:26 -07:00
parent 54bf485101
commit 61812343f5
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
7 changed files with 117 additions and 35 deletions

View file

@ -1,6 +1,9 @@
#include "iconimageprovider.hpp" #include "iconimageprovider.hpp"
#include <qcolor.h>
#include <qicon.h> #include <qicon.h>
#include <qlogging.h>
#include <qpainter.h>
#include <qpixmap.h> #include <qpixmap.h>
#include <qsize.h> #include <qsize.h>
#include <qstring.h> #include <qstring.h>
@ -10,8 +13,32 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re
auto icon = QIcon::fromTheme(id); auto icon = QIcon::fromTheme(id);
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
auto pixmap = icon.pixmap(targetSize.width(), targetSize.height()); auto pixmap = icon.pixmap(targetSize.width(), targetSize.height());
if (pixmap.isNull()) {
qWarning() << "Could not load icon" << id << "at size" << targetSize << "from request";
pixmap = IconImageProvider::missingPixmap(targetSize);
}
if (size != nullptr) *size = pixmap.size(); if (size != nullptr) *size = pixmap.size();
return pixmap; return pixmap;
} }
QPixmap IconImageProvider::missingPixmap(const QSize& size) {
auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1;
auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1;
if (width < 2) width = 2;
if (height < 2) height = 2;
auto pixmap = QPixmap(width, height);
pixmap.fill(QColorConstants::Black);
auto painter = QPainter(&pixmap);
auto halfWidth = width / 2;
auto halfHeight = height / 2;
auto purple = QColor(0xd900d8);
painter.fillRect(halfWidth, 0, halfWidth, halfHeight, purple);
painter.fillRect(0, halfHeight, halfWidth, halfHeight, purple);
return pixmap;
}

View file

@ -8,4 +8,6 @@ public:
explicit IconImageProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {} explicit IconImageProvider(): QQuickImageProvider(QQuickImageProvider::Pixmap) {}
QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override;
static QPixmap missingPixmap(const QSize& size);
}; };

View file

@ -25,11 +25,15 @@ Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarni
namespace qs::service::sni { namespace qs::service::sni {
StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent): QObject(parent) { StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent)
// spec is unclear about what exactly an item address is, so split off anything but the connection path : QObject(parent)
auto conn = address.split("/").value(0); , watcherId(address) {
this->item = // spec is unclear about what exactly an item address is, so account for both combinations
new DBusStatusNotifierItem(conn, "/StatusNotifierItem", QDBusConnection::sessionBus(), this); auto splitIdx = address.indexOf('/');
auto conn = splitIdx == -1 ? address : address.sliced(0, splitIdx);
auto path = splitIdx == -1 ? "/StatusNotifierItem" : address.sliced(splitIdx);
this->item = new DBusStatusNotifierItem(conn, path, QDBusConnection::sessionBus(), this);
if (!this->item->isValid()) { if (!this->item->isValid()) {
qCWarning(logStatusNotifierHost).noquote() << "Cannot create StatusNotifierItem for" << conn; qCWarning(logStatusNotifierHost).noquote() << "Cannot create StatusNotifierItem for" << conn;
@ -85,8 +89,7 @@ QString StatusNotifierItem::iconId() const {
if (!name.isEmpty() && overlayName.isEmpty()) return QString("image://icon/") + name; if (!name.isEmpty() && overlayName.isEmpty()) return QString("image://icon/") + name;
} }
return QString("image://service.sni/") + this->item->service() + "/" return QString("image://service.sni/") + this->watcherId + "/" + QString::number(this->iconIndex);
+ QString::number(this->iconIndex);
} }
QPixmap StatusNotifierItem::createPixmap(const QSize& size) const { QPixmap StatusNotifierItem::createPixmap(const QSize& size) const {

View file

@ -61,6 +61,7 @@ private:
// bumped to inhibit caching // bumped to inhibit caching
quint32 iconIndex = 0; quint32 iconIndex = 0;
QString watcherId;
}; };
} // namespace qs::service::sni } // namespace qs::service::sni

View file

@ -6,26 +6,37 @@
#include <qsize.h> #include <qsize.h>
#include <qstring.h> #include <qstring.h>
#include "../../core/iconimageprovider.hpp"
#include "host.hpp" #include "host.hpp"
namespace qs::service::sni { namespace qs::service::sni {
QPixmap QPixmap
TrayImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) { TrayImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) {
auto split = id.split('/'); QPixmap pixmap;
if (split.size() != 2) {
auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100);
if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2);
auto lastSplit = id.lastIndexOf('/');
if (lastSplit == -1) {
qCWarning(logStatusNotifierHost) << "Invalid image request:" << id; qCWarning(logStatusNotifierHost) << "Invalid image request:" << id;
return QPixmap(); } else {
auto path = id.sliced(0, lastSplit);
auto* item = StatusNotifierHost::instance()->itemByService(path);
if (item == nullptr) {
qCWarning(logStatusNotifierHost) << "Image requested for nonexistant service" << path;
} else {
pixmap = item->createPixmap(targetSize);
}
} }
auto* item = StatusNotifierHost::instance()->itemByService(split[0]); if (pixmap.isNull()) {
pixmap = IconImageProvider::missingPixmap(targetSize);
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(); if (size != nullptr) *size = pixmap.size();
return pixmap; return pixmap;
} }

View file

@ -68,17 +68,29 @@ void StatusNotifierWatcher::onServiceUnregistered(const QString& service) {
<< "Active StatusNotifierWatcher unregistered, attempting registration"; << "Active StatusNotifierWatcher unregistered, attempting registration";
this->tryRegister(); this->tryRegister();
return; 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 { } else {
qCWarning(logStatusNotifierWatcher).noquote() QString qualifiedItem;
<< "Got service unregister event for untracked service" << service; this->items.removeIf([&](const QString& item) {
if (item.startsWith(service)) {
qualifiedItem = item;
return true;
} else return false;
});
if (!qualifiedItem.isEmpty()) {
qCDebug(logStatusNotifierWatcher).noquote()
<< "Unregistered StatusNotifierItem" << qualifiedItem << "from watcher";
emit this->StatusNotifierItemUnregistered(qualifiedItem);
} 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); this->serviceWatcher.removeWatchedService(service);
@ -112,23 +124,44 @@ void StatusNotifierWatcher::RegisterStatusNotifierHost(const QString& host) {
} }
void StatusNotifierWatcher::RegisterStatusNotifierItem(const QString& item) { void StatusNotifierWatcher::RegisterStatusNotifierItem(const QString& item) {
if (this->items.contains(item)) { auto qualifiedItem = this->qualifiedItem(item);
auto service = qualifiedItem.split("/").at(0);
if (this->items.contains(qualifiedItem)) {
qCDebug(logStatusNotifierWatcher).noquote() qCDebug(logStatusNotifierWatcher).noquote()
<< "Skipping duplicate registration of StatusNotifierItem" << item << "to watcher"; << "Skipping duplicate registration of StatusNotifierItem" << qualifiedItem << "to watcher";
return; return;
} }
if (!QDBusConnection::sessionBus().interface()->serviceOwner(item).isValid()) { if (!QDBusConnection::sessionBus().interface()->serviceOwner(service).isValid()) {
qCWarning(logStatusNotifierWatcher).noquote() qCWarning(logStatusNotifierWatcher).noquote()
<< "Ignoring invalid StatusNotifierItem registration of" << item << "to watcher"; << "Ignoring invalid StatusNotifierItem registration of" << qualifiedItem << "to watcher";
return; return;
} }
this->serviceWatcher.addWatchedService(item); this->serviceWatcher.addWatchedService(service);
this->items.push_back(item); this->items.push_back(qualifiedItem);
qCDebug(logStatusNotifierWatcher).noquote() qCDebug(logStatusNotifierWatcher).noquote()
<< "Registered StatusNotifierItem" << item << "to watcher"; << "Registered StatusNotifierItem" << qualifiedItem << "to watcher";
emit this->StatusNotifierItemRegistered(item);
emit this->StatusNotifierItemRegistered(qualifiedItem);
}
QString StatusNotifierWatcher::qualifiedItem(const QString& item) {
// Registered items are often missing either the service id or the path.
QString qualifiedItem;
if (item.startsWith("/")) {
qualifiedItem = this->message().service() + item;
} else {
qualifiedItem = item;
}
if (!qualifiedItem.contains("/")) {
qualifiedItem += "/StatusNotifierItem";
}
return qualifiedItem;
} }
StatusNotifierWatcher* StatusNotifierWatcher::instance() { StatusNotifierWatcher* StatusNotifierWatcher::instance() {

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <qdbuscontext.h>
#include <qdbusinterface.h> #include <qdbusinterface.h>
#include <qdbusservicewatcher.h> #include <qdbusservicewatcher.h>
#include <qlist.h> #include <qlist.h>
@ -12,7 +13,9 @@ Q_DECLARE_LOGGING_CATEGORY(logStatusNotifierWatcher);
namespace qs::service::sni { namespace qs::service::sni {
class StatusNotifierWatcher: public QObject { class StatusNotifierWatcher
: public QObject
, protected QDBusContext {
Q_OBJECT; Q_OBJECT;
Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion); Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion);
Q_PROPERTY(bool IsStatusNotifierHostRegistered READ isHostRegistered); Q_PROPERTY(bool IsStatusNotifierHostRegistered READ isHostRegistered);
@ -46,6 +49,8 @@ private slots:
void onServiceUnregistered(const QString& service); void onServiceUnregistered(const QString& service);
private: private:
QString qualifiedItem(const QString& item);
QDBusServiceWatcher serviceWatcher; QDBusServiceWatcher serviceWatcher;
QList<QString> hosts; QList<QString> hosts;
QList<QString> items; QList<QString> items;