File indexing completed on 2024-05-12 05:37:09
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 ¬ifierItemId, 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 Plasma5Support::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, [=, 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 (qobject_cast<QApplication *>(QCoreApplication::instance()) && !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(std::move(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 }