File indexing completed on 2024-05-12 17:08:50

0001 /*
0002     SPDX-FileCopyrightText: 2009 Marco Martin <notmart@gmail.com>
0003     SPDX-FileCopyrightText: 2009 Matthieu Gallien <matthieu_gallien@yahoo.fr>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 #include "statusnotifieritemsource.h"
0009 #include "statusnotifieritemservice.h"
0010 #include "systemtraytypes.h"
0011 
0012 #include "debug.h"
0013 
0014 #include <KIconEngine>
0015 #include <KIconLoader>
0016 #include <QApplication>
0017 #include <QDBusMessage>
0018 #include <QDBusPendingCall>
0019 #include <QDBusPendingReply>
0020 #include <QDebug>
0021 #include <QIcon>
0022 #include <QImage>
0023 #include <QPainter>
0024 #include <QPixmap>
0025 #include <QSysInfo>
0026 #include <QVariantMap>
0027 
0028 #include <netinet/in.h>
0029 
0030 #include <dbusmenuimporter.h>
0031 
0032 class PlasmaDBusMenuImporter : public DBusMenuImporter
0033 {
0034 public:
0035     PlasmaDBusMenuImporter(const QString &service, const QString &path, KIconLoader *iconLoader, QObject *parent)
0036         : DBusMenuImporter(service, path, parent)
0037         , m_iconLoader(iconLoader)
0038     {
0039     }
0040 
0041 protected:
0042     QIcon iconForName(const QString &name) override
0043     {
0044         return QIcon(new KIconEngine(name, m_iconLoader));
0045     }
0046 
0047 private:
0048     KIconLoader *m_iconLoader;
0049 };
0050 
0051 StatusNotifierItemSource::StatusNotifierItemSource(const QString &notifierItemId, QObject *parent)
0052     : QObject(parent)
0053     , m_customIconLoader(nullptr)
0054     , m_menuImporter(nullptr)
0055     , m_refreshing(false)
0056     , m_needsReRefreshing(false)
0057 {
0058     setObjectName(notifierItemId);
0059     qDBusRegisterMetaType<KDbusImageStruct>();
0060     qDBusRegisterMetaType<KDbusImageVector>();
0061     qDBusRegisterMetaType<KDbusToolTipStruct>();
0062 
0063     m_servicename = notifierItemId;
0064 
0065     int slash = notifierItemId.indexOf('/');
0066     if (slash == -1) {
0067         qCWarning(SYSTEM_TRAY) << "Invalid notifierItemId:" << notifierItemId;
0068         m_valid = false;
0069         m_statusNotifierItemInterface = nullptr;
0070         return;
0071     }
0072     QString service = notifierItemId.left(slash);
0073     QString path = notifierItemId.mid(slash);
0074 
0075     m_statusNotifierItemInterface = new org::kde::StatusNotifierItem(service, path, QDBusConnection::sessionBus(), this);
0076 
0077     m_refreshTimer.setSingleShot(true);
0078     m_refreshTimer.setInterval(10);
0079     connect(&m_refreshTimer, &QTimer::timeout, this, &StatusNotifierItemSource::performRefresh);
0080 
0081     m_valid = !service.isEmpty() && m_statusNotifierItemInterface->isValid();
0082     if (m_valid) {
0083         connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewTitle, this, &StatusNotifierItemSource::refresh);
0084         connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewIcon, this, &StatusNotifierItemSource::refresh);
0085         connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewAttentionIcon, this, &StatusNotifierItemSource::refresh);
0086         connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewOverlayIcon, this, &StatusNotifierItemSource::refresh);
0087         connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewToolTip, this, &StatusNotifierItemSource::refresh);
0088         connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewStatus, this, &StatusNotifierItemSource::syncStatus);
0089         connect(m_statusNotifierItemInterface, &OrgKdeStatusNotifierItem::NewMenu, this, &StatusNotifierItemSource::refreshMenu);
0090         refresh();
0091     }
0092 }
0093 
0094 StatusNotifierItemSource::~StatusNotifierItemSource()
0095 {
0096     delete m_statusNotifierItemInterface;
0097 }
0098 
0099 KIconLoader *StatusNotifierItemSource::iconLoader() const
0100 {
0101     return m_customIconLoader ? m_customIconLoader : KIconLoader::global();
0102 }
0103 
0104 QIcon StatusNotifierItemSource::attentionIcon() const
0105 {
0106     return m_attentionIcon;
0107 }
0108 
0109 QString StatusNotifierItemSource::attentionIconName() const
0110 {
0111     return m_attentionIconName;
0112 }
0113 
0114 QString StatusNotifierItemSource::attentionMovieName() const
0115 {
0116     return m_attentionMovieName;
0117 }
0118 
0119 QString StatusNotifierItemSource::category() const
0120 {
0121     return m_category;
0122 }
0123 
0124 QIcon StatusNotifierItemSource::icon() const
0125 {
0126     return m_icon;
0127 }
0128 
0129 QString StatusNotifierItemSource::iconName() const
0130 {
0131     return m_iconName;
0132 }
0133 
0134 QString StatusNotifierItemSource::iconThemePath() const
0135 {
0136     return m_iconThemePath;
0137 }
0138 
0139 QString StatusNotifierItemSource::id() const
0140 {
0141     return m_id;
0142 }
0143 
0144 bool StatusNotifierItemSource::itemIsMenu() const
0145 {
0146     return m_itemIsMenu;
0147 }
0148 
0149 QString StatusNotifierItemSource::overlayIconName() const
0150 {
0151     return m_overlayIconName;
0152 }
0153 
0154 QString StatusNotifierItemSource::status() const
0155 {
0156     return m_status;
0157 }
0158 
0159 QString StatusNotifierItemSource::title() const
0160 {
0161     return m_title;
0162 }
0163 
0164 QVariant StatusNotifierItemSource::toolTipIcon() const
0165 {
0166     return m_toolTipIcon;
0167 }
0168 
0169 QString StatusNotifierItemSource::toolTipSubTitle() const
0170 {
0171     return m_toolTipSubTitle;
0172 }
0173 
0174 QString StatusNotifierItemSource::toolTipTitle() const
0175 {
0176     return m_toolTipTitle;
0177 }
0178 
0179 QString StatusNotifierItemSource::windowId() const
0180 {
0181     return m_windowId;
0182 }
0183 
0184 Plasma::Service *StatusNotifierItemSource::createService()
0185 {
0186     return new StatusNotifierItemService(this);
0187 }
0188 
0189 void StatusNotifierItemSource::syncStatus(const QString &status)
0190 {
0191     m_status = status;
0192     Q_EMIT dataUpdated();
0193 }
0194 
0195 void StatusNotifierItemSource::refreshMenu()
0196 {
0197     if (m_menuImporter) {
0198         delete m_menuImporter;
0199         m_menuImporter = nullptr;
0200     }
0201     refresh();
0202 }
0203 
0204 void StatusNotifierItemSource::refresh()
0205 {
0206     if (!m_refreshTimer.isActive()) {
0207         m_refreshTimer.start();
0208     }
0209 }
0210 
0211 void StatusNotifierItemSource::performRefresh()
0212 {
0213     if (m_refreshing) {
0214         m_needsReRefreshing = true;
0215         return;
0216     }
0217 
0218     m_refreshing = true;
0219     QDBusMessage message = QDBusMessage::createMethodCall(m_statusNotifierItemInterface->service(),
0220                                                           m_statusNotifierItemInterface->path(),
0221                                                           QStringLiteral("org.freedesktop.DBus.Properties"),
0222                                                           QStringLiteral("GetAll"));
0223 
0224     message << m_statusNotifierItemInterface->interface();
0225     QDBusPendingCall call = m_statusNotifierItemInterface->connection().asyncCall(message);
0226     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
0227     connect(watcher, &QDBusPendingCallWatcher::finished, this, &StatusNotifierItemSource::refreshCallback);
0228 }
0229 
0230 /**
0231   \todo add a smart pointer to guard call and to automatically delete it at the end of the function
0232   */
0233 void StatusNotifierItemSource::refreshCallback(QDBusPendingCallWatcher *call)
0234 {
0235     m_refreshing = false;
0236     if (m_needsReRefreshing) {
0237         m_needsReRefreshing = false;
0238         performRefresh();
0239         call->deleteLater();
0240         return;
0241     }
0242 
0243     QDBusPendingReply<QVariantMap> reply = *call;
0244     if (reply.isError()) {
0245         m_valid = false;
0246     } else {
0247         // IconThemePath (handle this one first, because it has an impact on
0248         // others)
0249         QVariantMap properties = reply.argumentAt<0>();
0250         QString path = properties[QStringLiteral("IconThemePath")].toString();
0251 
0252         if (!path.isEmpty() && path != m_iconThemePath) {
0253             if (!m_customIconLoader) {
0254                 m_customIconLoader = new KIconLoader(QString(), QStringList(), this);
0255             }
0256             // FIXME: If last part of path is not "icons", this won't work!
0257             QString appName;
0258             auto tokens = QStringView(path).split('/', Qt::SkipEmptyParts);
0259             if (tokens.length() >= 3 && tokens.takeLast() == QLatin1String("icons"))
0260                 appName = tokens.takeLast().toString();
0261 
0262             // icons may be either in the root directory of the passed path or in a appdir format
0263             // i.e hicolor/32x32/iconname.png
0264 
0265             m_customIconLoader->reconfigure(appName, QStringList(path));
0266 
0267             // add app dir requires an app name, though this is completely unused in this context
0268             m_customIconLoader->addAppDir(appName.size() ? appName : QStringLiteral("unused"), path);
0269 
0270             connect(m_customIconLoader, &KIconLoader::iconChanged, this, [=] {
0271                 m_customIconLoader->reconfigure(appName, QStringList(path));
0272                 m_customIconLoader->addAppDir(appName.size() ? appName : QStringLiteral("unused"), path);
0273             });
0274         }
0275         m_iconThemePath = path;
0276 
0277         m_category = properties[QStringLiteral("Category")].toString();
0278         m_status = properties[QStringLiteral("Status")].toString();
0279         m_title = properties[QStringLiteral("Title")].toString();
0280         m_id = properties[QStringLiteral("Id")].toString();
0281         m_windowId = properties[QStringLiteral("WindowId")].toString();
0282         m_itemIsMenu = properties[QStringLiteral("ItemIsMenu")].toBool();
0283 
0284         // Attention Movie
0285         m_attentionMovieName = properties[QStringLiteral("AttentionMovieName")].toString();
0286 
0287         QIcon overlay;
0288         QStringList overlayNames;
0289 
0290         // Overlay icon
0291         {
0292             m_overlayIconName = QString();
0293 
0294             const QString iconName = properties[QStringLiteral("OverlayIconName")].toString();
0295             if (!iconName.isEmpty()) {
0296                 overlay = QIcon(new KIconEngine(iconName, iconLoader()));
0297                 if (!overlay.isNull()) {
0298                     m_overlayIconName = iconName;
0299                     overlayNames << iconName;
0300                 }
0301             }
0302             if (overlay.isNull()) {
0303                 KDbusImageVector image;
0304                 properties[QStringLiteral("OverlayIconPixmap")].value<QDBusArgument>() >> image;
0305                 if (!image.isEmpty()) {
0306                     overlay = imageVectorToPixmap(image);
0307                 }
0308             }
0309         }
0310 
0311         auto loadIcon = [this, &properties, &overlay, &overlayNames](const QString &iconKey, const QString &pixmapKey) -> std::tuple<QIcon, QString> {
0312             const QString iconName = properties[iconKey].toString();
0313             if (!iconName.isEmpty()) {
0314                 QIcon icon = QIcon(new KIconEngine(iconName, iconLoader(), overlayNames));
0315                 if (!icon.isNull()) {
0316                     if (!overlay.isNull() && overlayNames.isEmpty()) {
0317                         overlayIcon(&icon, &overlay);
0318                     }
0319                     return {icon, iconName};
0320                 }
0321             }
0322             KDbusImageVector image;
0323             properties[pixmapKey].value<QDBusArgument>() >> image;
0324             if (!image.isEmpty()) {
0325                 QIcon icon = imageVectorToPixmap(image);
0326                 if (!icon.isNull() && !overlay.isNull()) {
0327                     overlayIcon(&icon, &overlay);
0328                 }
0329                 return {icon, QString()};
0330             }
0331             return {};
0332         };
0333 
0334         std::tie(m_icon, m_iconName) = loadIcon(QStringLiteral("IconName"), QStringLiteral("IconPixmap"));
0335         std::tie(m_attentionIcon, m_attentionIconName) = loadIcon(QStringLiteral("AttentionIconName"), QStringLiteral("AttentionIconPixmap"));
0336 
0337         // ToolTip
0338         {
0339             KDbusToolTipStruct toolTip;
0340             properties[QStringLiteral("ToolTip")].value<QDBusArgument>() >> toolTip;
0341             if (toolTip.title.isEmpty()) {
0342                 m_toolTipTitle = QString();
0343                 m_toolTipSubTitle = QString();
0344                 m_toolTipIcon = QString();
0345             } else {
0346                 QIcon toolTipIcon;
0347                 if (toolTip.image.size() == 0) {
0348                     toolTipIcon = QIcon(new KIconEngine(toolTip.icon, iconLoader()));
0349                 } else {
0350                     toolTipIcon = imageVectorToPixmap(toolTip.image);
0351                 }
0352                 m_toolTipTitle = toolTip.title;
0353                 m_toolTipSubTitle = toolTip.subTitle;
0354                 if (toolTipIcon.isNull() || toolTipIcon.availableSizes().isEmpty()) {
0355                     m_toolTipIcon = QString();
0356                 } else {
0357                     m_toolTipIcon = toolTipIcon;
0358                 }
0359             }
0360         }
0361 
0362         // Menu
0363         if (!m_menuImporter) {
0364             QString menuObjectPath = properties[QStringLiteral("Menu")].value<QDBusObjectPath>().path();
0365             if (!menuObjectPath.isEmpty()) {
0366                 if (menuObjectPath == QLatin1String("/NO_DBUSMENU")) {
0367                     // This is a hack to make it possible to disable DBusMenu in an
0368                     // application. The string "/NO_DBUSMENU" must be the same as in
0369                     // KStatusNotifierItem::setContextMenu().
0370                     qCWarning(SYSTEM_TRAY) << "DBusMenu disabled for this application";
0371                 } else {
0372                     m_menuImporter = new PlasmaDBusMenuImporter(m_statusNotifierItemInterface->service(), menuObjectPath, iconLoader(), this);
0373                     connect(m_menuImporter, &PlasmaDBusMenuImporter::menuUpdated, this, [this](QMenu *menu) {
0374                         if (menu == m_menuImporter->menu()) {
0375                             contextMenuReady();
0376                         }
0377                     });
0378                 }
0379             }
0380         }
0381     }
0382 
0383     Q_EMIT dataUpdated();
0384     call->deleteLater();
0385 }
0386 
0387 void StatusNotifierItemSource::contextMenuReady()
0388 {
0389     Q_EMIT contextMenuReady(m_menuImporter->menu());
0390 }
0391 
0392 QPixmap StatusNotifierItemSource::KDbusImageStructToPixmap(const KDbusImageStruct &image) const
0393 {
0394     // swap from network byte order if we are little endian
0395     if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
0396         uint *uintBuf = (uint *)image.data.data();
0397         for (uint i = 0; i < image.data.size() / sizeof(uint); ++i) {
0398             *uintBuf = ntohl(*uintBuf);
0399             ++uintBuf;
0400         }
0401     }
0402     if (image.width == 0 || image.height == 0) {
0403         return QPixmap();
0404     }
0405 
0406     // avoid a deep copy of the image data
0407     // we need to keep a reference to the image.data alive for the lifespan of the image, even if the image is copied
0408     // we create a new QByteArray with a shallow copy of the original data on the heap, then delete this in the QImage cleanup
0409     auto dataRef = new QByteArray(image.data);
0410 
0411     QImage iconImage(
0412         reinterpret_cast<const uchar *>(dataRef->data()),
0413         image.width,
0414         image.height,
0415         QImage::Format_ARGB32,
0416         [](void *ptr) {
0417             delete static_cast<QByteArray *>(ptr);
0418         },
0419         dataRef);
0420     return QPixmap::fromImage(iconImage);
0421 }
0422 
0423 QIcon StatusNotifierItemSource::imageVectorToPixmap(const KDbusImageVector &vector) const
0424 {
0425     QIcon icon;
0426 
0427     for (int i = 0; i < vector.size(); ++i) {
0428         icon.addPixmap(KDbusImageStructToPixmap(vector[i]));
0429     }
0430 
0431     return icon;
0432 }
0433 
0434 void StatusNotifierItemSource::overlayIcon(QIcon *icon, QIcon *overlay)
0435 {
0436     QIcon tmp;
0437     QPixmap m_iconPixmap = icon->pixmap(KIconLoader::SizeSmall, KIconLoader::SizeSmall);
0438 
0439     QPainter p(&m_iconPixmap);
0440 
0441     const int size = KIconLoader::SizeSmall / 2;
0442     p.drawPixmap(QRect(size, size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size));
0443     p.end();
0444     tmp.addPixmap(m_iconPixmap);
0445 
0446     // if an m_icon exactly that size wasn't found don't add it to the vector
0447     m_iconPixmap = icon->pixmap(KIconLoader::SizeSmallMedium, KIconLoader::SizeSmallMedium);
0448     if (m_iconPixmap.width() == KIconLoader::SizeSmallMedium) {
0449         const int size = KIconLoader::SizeSmall / 2;
0450         QPainter p(&m_iconPixmap);
0451         p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size));
0452         p.end();
0453         tmp.addPixmap(m_iconPixmap);
0454     }
0455 
0456     m_iconPixmap = icon->pixmap(KIconLoader::SizeMedium, KIconLoader::SizeMedium);
0457     if (m_iconPixmap.width() == KIconLoader::SizeMedium) {
0458         const int size = KIconLoader::SizeSmall / 2;
0459         QPainter p(&m_iconPixmap);
0460         p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size));
0461         p.end();
0462         tmp.addPixmap(m_iconPixmap);
0463     }
0464 
0465     m_iconPixmap = icon->pixmap(KIconLoader::SizeLarge, KIconLoader::SizeLarge);
0466     if (m_iconPixmap.width() == KIconLoader::SizeLarge) {
0467         const int size = KIconLoader::SizeSmall;
0468         QPainter p(&m_iconPixmap);
0469         p.drawPixmap(QRect(m_iconPixmap.width() - size, m_iconPixmap.height() - size, size, size), overlay->pixmap(size, size), QRect(0, 0, size, size));
0470         p.end();
0471         tmp.addPixmap(m_iconPixmap);
0472     }
0473 
0474     // We can't do 'm_icon->addPixmap()' because if 'm_icon' uses KIconEngine,
0475     // it will ignore the added pixmaps. This is not a bug in KIconEngine,
0476     // QIcon::addPixmap() doc says: "Custom m_icon engines are free to ignore
0477     // additionally added pixmaps".
0478     *icon = tmp;
0479     // hopefully huge and enormous not necessary right now, since it's quite costly
0480 }
0481 
0482 void StatusNotifierItemSource::activate(int x, int y)
0483 {
0484     if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) {
0485         QDBusMessage message = QDBusMessage::createMethodCall(m_statusNotifierItemInterface->service(),
0486                                                               m_statusNotifierItemInterface->path(),
0487                                                               m_statusNotifierItemInterface->interface(),
0488                                                               QStringLiteral("Activate"));
0489 
0490         message << x << y;
0491         QDBusPendingCall call = m_statusNotifierItemInterface->connection().asyncCall(message);
0492         QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(call, this);
0493         connect(watcher, &QDBusPendingCallWatcher::finished, this, &StatusNotifierItemSource::activateCallback);
0494     }
0495 }
0496 
0497 void StatusNotifierItemSource::activateCallback(QDBusPendingCallWatcher *call)
0498 {
0499     QDBusPendingReply<void> reply = *call;
0500     Q_EMIT activateResult(!reply.isError());
0501     call->deleteLater();
0502 }
0503 
0504 void StatusNotifierItemSource::secondaryActivate(int x, int y)
0505 {
0506     if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) {
0507         m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("SecondaryActivate"), x, y);
0508     }
0509 }
0510 
0511 void StatusNotifierItemSource::scroll(int delta, const QString &direction)
0512 {
0513     if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) {
0514         m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("Scroll"), delta, direction);
0515     }
0516 }
0517 
0518 void StatusNotifierItemSource::contextMenu(int x, int y)
0519 {
0520     if (m_menuImporter) {
0521         m_menuImporter->updateMenu();
0522     } else {
0523         qCWarning(SYSTEM_TRAY) << "Could not find DBusMenu interface, falling back to calling ContextMenu()";
0524         if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) {
0525             m_statusNotifierItemInterface->call(QDBus::NoBlock, QStringLiteral("ContextMenu"), x, y);
0526         }
0527     }
0528 }
0529 
0530 void StatusNotifierItemSource::provideXdgActivationToken(const QString &token)
0531 {
0532     if (m_statusNotifierItemInterface && m_statusNotifierItemInterface->isValid()) {
0533         m_statusNotifierItemInterface->ProvideXdgActivationToken(token);
0534     }
0535 }