#include "item.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../core/iconimageprovider.hpp" #include "../../core/imageprovider.hpp" #include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "dbus_item.h" #include "dbus_item_types.hpp" #include "host.hpp" using namespace qs::dbus; using namespace qs::dbus::dbusmenu; Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg); Q_LOGGING_CATEGORY(logSniMenu, "quickshell.service.sni.item.menu", QtWarningMsg); namespace qs::service::sni { StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent) : QObject(parent) , watcherId(address) { qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); // spec is unclear about what exactly an item address is, so account for both combinations 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()) { qCWarning(logStatusNotifierHost).noquote() << "Cannot create StatusNotifierItem for" << conn; return; } // 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->iconThemePath, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon); 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); QObject::connect(&this->menuPath, &AbstractDBusProperty::changed, this, &StatusNotifierItem::onMenuPathChanged); // 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 IconImageProvider::requestString(name, this->iconThemePath.get()); } else { auto name = this->iconName.get(); auto overlayName = this->overlayIconName.get(); if (!name.isEmpty() && overlayName.isEmpty()) return IconImageProvider::requestString(name, this->iconThemePath.get()); } return this->imageHandle.url() + "/" + 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) { const auto image = icon->createImage().scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); pixmap = QPixmap::fromImage(image); } } } 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) { const auto image = icon->createImage().scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); pixmap = QPixmap::fromImage(image); } } 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) { const auto image = icon->createImage().scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); overlay = QPixmap::fromImage(image); } } if (!overlay.isNull()) { auto painter = QPainter(&pixmap); painter.drawPixmap(QRect(0, 0, pixmap.width(), pixmap.height()), overlay); painter.end(); } } return pixmap; } void StatusNotifierItem::activate() { auto pendingCall = this->item->Activate(0, 0); auto* call = new QDBusPendingCallWatcher(pendingCall, this); auto responseCallback = [this](QDBusPendingCallWatcher* call) { const QDBusPendingReply<> reply = *call; if (reply.isError()) { qCWarning(logStatusNotifierItem).noquote() << "Error calling Activate method of StatusNotifierItem" << this->properties.toString(); qCWarning(logStatusNotifierItem) << reply.error(); } delete call; }; QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } void StatusNotifierItem::secondaryActivate() { auto pendingCall = this->item->SecondaryActivate(0, 0); auto* call = new QDBusPendingCallWatcher(pendingCall, this); auto responseCallback = [this](QDBusPendingCallWatcher* call) { const QDBusPendingReply<> reply = *call; if (reply.isError()) { qCWarning(logStatusNotifierItem).noquote() << "Error calling SecondaryActivate method of StatusNotifierItem" << this->properties.toString(); qCWarning(logStatusNotifierItem) << reply.error(); } delete call; }; QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } void StatusNotifierItem::scroll(qint32 delta, bool horizontal) { this->item->Scroll(delta, horizontal ? "horizontal" : "vertical"); } void StatusNotifierItem::updateIcon() { this->iconIndex++; emit this->iconChanged(); } DBusMenu* StatusNotifierItem::menu() const { return this->mMenu; } void StatusNotifierItem::refMenu() { this->menuRefcount++; qCDebug(logSniMenu) << "Menu of" << this << "gained a reference. Refcount is now" << this->menuRefcount; if (this->menuRefcount == 1) { this->onMenuPathChanged(); } else { // Refresh the layout when opening a menu in case a bad client isn't updating it // and another ref is open somewhere. this->mMenu->rootItem.updateLayout(); } } void StatusNotifierItem::unrefMenu() { this->menuRefcount--; qCDebug(logSniMenu) << "Menu of" << this << "lost a reference. Refcount is now" << this->menuRefcount; if (this->menuRefcount == 0) { this->onMenuPathChanged(); } } void StatusNotifierItem::onMenuPathChanged() { qCDebug(logSniMenu) << "Updating menu of" << this << "with refcount" << this->menuRefcount << "path" << this->menuPath.get().path(); if (this->mMenu) { this->mMenu->deleteLater(); this->mMenu = nullptr; } if (this->menuRefcount > 0 && !this->menuPath.get().path().isEmpty()) { this->mMenu = new DBusMenu(this->item->service(), this->menuPath.get().path()); this->mMenu->setParent(this); this->mMenu->rootItem.setShowChildrenRecursive(true); } emit this->menuChanged(); } void StatusNotifierItem::onGetAllFinished() { if (this->mReady) return; this->mReady = true; emit this->ready(); } TrayImageHandle::TrayImageHandle(StatusNotifierItem* item) : QsImageHandle(QQmlImageProviderBase::Pixmap, item) , item(item) {} QPixmap TrayImageHandle::requestPixmap(const QString& /*unused*/, QSize* size, const QSize& requestedSize) { auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2); auto pixmap = this->item->createPixmap(targetSize); if (pixmap.isNull()) { pixmap = IconImageProvider::missingPixmap(targetSize); } if (size != nullptr) *size = pixmap.size(); return pixmap; } } // namespace qs::service::sni