File indexing completed on 2024-04-28 15:29:11
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2009 Marco Martin <notmart@gmail.com> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "kstatusnotifieritem.h" 0009 #include "config-knotifications.h" 0010 #include "debug_p.h" 0011 #include "kstatusnotifieritemprivate_p.h" 0012 0013 #include <QApplication> 0014 #include <QImage> 0015 #include <QMenu> 0016 #include <QMessageBox> 0017 #include <QMovie> 0018 #include <QPainter> 0019 #include <QPixmap> 0020 #include <QPushButton> 0021 #include <QStandardPaths> 0022 #ifdef Q_OS_MACOS 0023 #include <QFontDatabase> 0024 #include <QtMac> 0025 #endif 0026 0027 #ifdef QT_DBUS_LIB 0028 #include "kstatusnotifieritemdbus_p.h" 0029 0030 #include <QDBusConnection> 0031 0032 #if HAVE_DBUSMENUQT 0033 #include <dbusmenuexporter.h> 0034 #endif // HAVE_DBUSMENUQT 0035 #endif 0036 0037 #include <KWindowInfo> 0038 #include <QTimer> 0039 #include <kwindowsystem.h> 0040 0041 #include <cstdlib> 0042 0043 static const char s_statusNotifierWatcherServiceName[] = "org.kde.StatusNotifierWatcher"; 0044 static const int s_legacyTrayIconSize = 24; 0045 0046 KStatusNotifierItem::KStatusNotifierItem(QObject *parent) 0047 : QObject(parent) 0048 , d(new KStatusNotifierItemPrivate(this)) 0049 { 0050 d->init(QString()); 0051 } 0052 0053 KStatusNotifierItem::KStatusNotifierItem(const QString &id, QObject *parent) 0054 : QObject(parent) 0055 , d(new KStatusNotifierItemPrivate(this)) 0056 { 0057 d->init(id); 0058 } 0059 0060 KStatusNotifierItem::~KStatusNotifierItem() 0061 { 0062 #ifdef QT_DBUS_LIB 0063 delete d->statusNotifierWatcher; 0064 delete d->notificationsClient; 0065 #endif 0066 delete d->systemTrayIcon; 0067 if (!qApp->closingDown()) { 0068 delete d->menu; 0069 } 0070 if (d->associatedWidget) { 0071 KWindowSystem::self()->disconnect(d->associatedWidget); 0072 } 0073 } 0074 0075 QString KStatusNotifierItem::id() const 0076 { 0077 // qCDebug(LOG_KNOTIFICATIONS) << "id requested" << d->id; 0078 return d->id; 0079 } 0080 0081 void KStatusNotifierItem::setCategory(const ItemCategory category) 0082 { 0083 d->category = category; 0084 } 0085 0086 KStatusNotifierItem::ItemStatus KStatusNotifierItem::status() const 0087 { 0088 return d->status; 0089 } 0090 0091 KStatusNotifierItem::ItemCategory KStatusNotifierItem::category() const 0092 { 0093 return d->category; 0094 } 0095 0096 void KStatusNotifierItem::setTitle(const QString &title) 0097 { 0098 d->title = title; 0099 } 0100 0101 void KStatusNotifierItem::setStatus(const ItemStatus status) 0102 { 0103 if (d->status == status) { 0104 return; 0105 } 0106 0107 d->status = status; 0108 0109 #ifdef QT_DBUS_LIB 0110 Q_EMIT d->statusNotifierItemDBus->NewStatus( 0111 QString::fromLatin1(metaObject()->enumerator(metaObject()->indexOfEnumerator("ItemStatus")).valueToKey(d->status))); 0112 #endif 0113 if (d->systemTrayIcon) { 0114 d->syncLegacySystemTrayIcon(); 0115 } 0116 } 0117 0118 // normal icon 0119 0120 void KStatusNotifierItem::setIconByName(const QString &name) 0121 { 0122 if (d->iconName == name) { 0123 return; 0124 } 0125 0126 d->iconName = name; 0127 0128 #ifdef QT_DBUS_LIB 0129 d->serializedIcon = KDbusImageVector(); 0130 Q_EMIT d->statusNotifierItemDBus->NewIcon(); 0131 #endif 0132 0133 if (d->systemTrayIcon) { 0134 d->systemTrayIcon->setIcon(QIcon::fromTheme(name)); 0135 } 0136 } 0137 0138 QString KStatusNotifierItem::iconName() const 0139 { 0140 return d->iconName; 0141 } 0142 0143 void KStatusNotifierItem::setIconByPixmap(const QIcon &icon) 0144 { 0145 if (d->iconName.isEmpty() && d->icon.cacheKey() == icon.cacheKey()) { 0146 return; 0147 } 0148 0149 d->iconName.clear(); 0150 0151 #ifdef QT_DBUS_LIB 0152 d->serializedIcon = d->iconToVector(icon); 0153 Q_EMIT d->statusNotifierItemDBus->NewIcon(); 0154 #endif 0155 0156 d->icon = icon; 0157 if (d->systemTrayIcon) { 0158 d->systemTrayIcon->setIcon(icon); 0159 } 0160 } 0161 0162 QIcon KStatusNotifierItem::iconPixmap() const 0163 { 0164 return d->icon; 0165 } 0166 0167 void KStatusNotifierItem::setOverlayIconByName(const QString &name) 0168 { 0169 if (d->overlayIconName == name) { 0170 return; 0171 } 0172 0173 d->overlayIconName = name; 0174 #ifdef QT_DBUS_LIB 0175 Q_EMIT d->statusNotifierItemDBus->NewOverlayIcon(); 0176 #endif 0177 if (d->systemTrayIcon) { 0178 QPixmap iconPixmap = QIcon::fromTheme(d->iconName).pixmap(s_legacyTrayIconSize, s_legacyTrayIconSize); 0179 if (!name.isEmpty()) { 0180 QPixmap overlayPixmap = QIcon::fromTheme(d->overlayIconName).pixmap(s_legacyTrayIconSize / 2, s_legacyTrayIconSize / 2); 0181 QPainter p(&iconPixmap); 0182 p.drawPixmap(iconPixmap.width() - overlayPixmap.width(), iconPixmap.height() - overlayPixmap.height(), overlayPixmap); 0183 p.end(); 0184 } 0185 d->systemTrayIcon->setIcon(iconPixmap); 0186 } 0187 } 0188 0189 QString KStatusNotifierItem::overlayIconName() const 0190 { 0191 return d->overlayIconName; 0192 } 0193 0194 void KStatusNotifierItem::setOverlayIconByPixmap(const QIcon &icon) 0195 { 0196 if (d->overlayIconName.isEmpty() && d->overlayIcon.cacheKey() == icon.cacheKey()) { 0197 return; 0198 } 0199 0200 d->overlayIconName.clear(); 0201 0202 #ifdef QT_DBUS_LIB 0203 d->serializedOverlayIcon = d->iconToVector(icon); 0204 Q_EMIT d->statusNotifierItemDBus->NewOverlayIcon(); 0205 #endif 0206 0207 d->overlayIcon = icon; 0208 if (d->systemTrayIcon) { 0209 QPixmap iconPixmap = d->icon.pixmap(s_legacyTrayIconSize, s_legacyTrayIconSize); 0210 QPixmap overlayPixmap = d->overlayIcon.pixmap(s_legacyTrayIconSize / 2, s_legacyTrayIconSize / 2); 0211 0212 QPainter p(&iconPixmap); 0213 p.drawPixmap(iconPixmap.width() - overlayPixmap.width(), iconPixmap.height() - overlayPixmap.height(), overlayPixmap); 0214 p.end(); 0215 d->systemTrayIcon->setIcon(iconPixmap); 0216 } 0217 } 0218 0219 QIcon KStatusNotifierItem::overlayIconPixmap() const 0220 { 0221 return d->overlayIcon; 0222 } 0223 0224 // Icons and movie for requesting attention state 0225 0226 void KStatusNotifierItem::setAttentionIconByName(const QString &name) 0227 { 0228 if (d->attentionIconName == name) { 0229 return; 0230 } 0231 0232 d->attentionIconName = name; 0233 0234 #ifdef QT_DBUS_LIB 0235 d->serializedAttentionIcon = KDbusImageVector(); 0236 Q_EMIT d->statusNotifierItemDBus->NewAttentionIcon(); 0237 #endif 0238 } 0239 0240 QString KStatusNotifierItem::attentionIconName() const 0241 { 0242 return d->attentionIconName; 0243 } 0244 0245 void KStatusNotifierItem::setAttentionIconByPixmap(const QIcon &icon) 0246 { 0247 if (d->attentionIconName.isEmpty() && d->attentionIcon.cacheKey() == icon.cacheKey()) { 0248 return; 0249 } 0250 0251 d->attentionIconName.clear(); 0252 d->attentionIcon = icon; 0253 0254 #ifdef QT_DBUS_LIB 0255 d->serializedAttentionIcon = d->iconToVector(icon); 0256 Q_EMIT d->statusNotifierItemDBus->NewAttentionIcon(); 0257 #endif 0258 } 0259 0260 QIcon KStatusNotifierItem::attentionIconPixmap() const 0261 { 0262 return d->attentionIcon; 0263 } 0264 0265 void KStatusNotifierItem::setAttentionMovieByName(const QString &name) 0266 { 0267 if (d->movieName == name) { 0268 return; 0269 } 0270 0271 d->movieName = name; 0272 0273 delete d->movie; 0274 d->movie = nullptr; 0275 0276 #ifdef QT_DBUS_LIB 0277 Q_EMIT d->statusNotifierItemDBus->NewAttentionIcon(); 0278 #endif 0279 0280 if (d->systemTrayIcon) { 0281 d->movie = new QMovie(d->movieName); 0282 d->systemTrayIcon->setMovie(d->movie); 0283 } 0284 } 0285 0286 QString KStatusNotifierItem::attentionMovieName() const 0287 { 0288 return d->movieName; 0289 } 0290 0291 // ToolTip 0292 0293 #ifdef Q_OS_MACOS 0294 static void setTrayToolTip(KStatusNotifierLegacyIcon *systemTrayIcon, const QString &title, const QString &subTitle) 0295 { 0296 if (systemTrayIcon) { 0297 bool tEmpty = title.isEmpty(), stEmpty = subTitle.isEmpty(); 0298 if (tEmpty) { 0299 if (!stEmpty) { 0300 systemTrayIcon->setToolTip(subTitle); 0301 } else { 0302 systemTrayIcon->setToolTip(title); 0303 } 0304 } else { 0305 if (stEmpty) { 0306 systemTrayIcon->setToolTip(title); 0307 } else { 0308 systemTrayIcon->setToolTip(title + QStringLiteral("\n") + subTitle); 0309 } 0310 } 0311 } 0312 } 0313 #else 0314 static void setTrayToolTip(KStatusNotifierLegacyIcon *systemTrayIcon, const QString &title, const QString &) 0315 { 0316 if (systemTrayIcon) { 0317 systemTrayIcon->setToolTip(title); 0318 } 0319 } 0320 #endif 0321 0322 void KStatusNotifierItem::setToolTip(const QString &iconName, const QString &title, const QString &subTitle) 0323 { 0324 if (d->toolTipIconName == iconName && d->toolTipTitle == title && d->toolTipSubTitle == subTitle) { 0325 return; 0326 } 0327 0328 d->toolTipIconName = iconName; 0329 0330 d->toolTipTitle = title; 0331 setTrayToolTip(d->systemTrayIcon, title, subTitle); 0332 d->toolTipSubTitle = subTitle; 0333 0334 #ifdef QT_DBUS_LIB 0335 d->serializedToolTipIcon = KDbusImageVector(); 0336 Q_EMIT d->statusNotifierItemDBus->NewToolTip(); 0337 #endif 0338 } 0339 0340 void KStatusNotifierItem::setToolTip(const QIcon &icon, const QString &title, const QString &subTitle) 0341 { 0342 if (d->toolTipIconName.isEmpty() && d->toolTipIcon.cacheKey() == icon.cacheKey() // 0343 && d->toolTipTitle == title // 0344 && d->toolTipSubTitle == subTitle) { 0345 return; 0346 } 0347 0348 d->toolTipIconName.clear(); 0349 d->toolTipIcon = icon; 0350 0351 d->toolTipTitle = title; 0352 setTrayToolTip(d->systemTrayIcon, title, subTitle); 0353 0354 d->toolTipSubTitle = subTitle; 0355 #ifdef QT_DBUS_LIB 0356 d->serializedToolTipIcon = d->iconToVector(icon); 0357 Q_EMIT d->statusNotifierItemDBus->NewToolTip(); 0358 #endif 0359 } 0360 0361 void KStatusNotifierItem::setToolTipIconByName(const QString &name) 0362 { 0363 if (d->toolTipIconName == name) { 0364 return; 0365 } 0366 0367 d->toolTipIconName = name; 0368 #ifdef QT_DBUS_LIB 0369 d->serializedToolTipIcon = KDbusImageVector(); 0370 Q_EMIT d->statusNotifierItemDBus->NewToolTip(); 0371 #endif 0372 } 0373 0374 QString KStatusNotifierItem::toolTipIconName() const 0375 { 0376 return d->toolTipIconName; 0377 } 0378 0379 void KStatusNotifierItem::setToolTipIconByPixmap(const QIcon &icon) 0380 { 0381 if (d->toolTipIconName.isEmpty() && d->toolTipIcon.cacheKey() == icon.cacheKey()) { 0382 return; 0383 } 0384 0385 d->toolTipIconName.clear(); 0386 d->toolTipIcon = icon; 0387 0388 #ifdef QT_DBUS_LIB 0389 d->serializedToolTipIcon = d->iconToVector(icon); 0390 Q_EMIT d->statusNotifierItemDBus->NewToolTip(); 0391 #endif 0392 } 0393 0394 QIcon KStatusNotifierItem::toolTipIconPixmap() const 0395 { 0396 return d->toolTipIcon; 0397 } 0398 0399 void KStatusNotifierItem::setToolTipTitle(const QString &title) 0400 { 0401 if (d->toolTipTitle == title) { 0402 return; 0403 } 0404 0405 d->toolTipTitle = title; 0406 0407 #ifdef QT_DBUS_LIB 0408 Q_EMIT d->statusNotifierItemDBus->NewToolTip(); 0409 #endif 0410 setTrayToolTip(d->systemTrayIcon, title, d->toolTipSubTitle); 0411 } 0412 0413 QString KStatusNotifierItem::toolTipTitle() const 0414 { 0415 return d->toolTipTitle; 0416 } 0417 0418 void KStatusNotifierItem::setToolTipSubTitle(const QString &subTitle) 0419 { 0420 if (d->toolTipSubTitle == subTitle) { 0421 return; 0422 } 0423 0424 d->toolTipSubTitle = subTitle; 0425 #ifdef QT_DBUS_LIB 0426 Q_EMIT d->statusNotifierItemDBus->NewToolTip(); 0427 #else 0428 setTrayToolTip(d->systemTrayIcon, d->toolTipTitle, subTitle); 0429 #endif 0430 } 0431 0432 QString KStatusNotifierItem::toolTipSubTitle() const 0433 { 0434 return d->toolTipSubTitle; 0435 } 0436 0437 void KStatusNotifierItem::setContextMenu(QMenu *menu) 0438 { 0439 if (d->menu && d->menu != menu) { 0440 d->menu->removeEventFilter(this); 0441 delete d->menu; 0442 } 0443 0444 if (!menu) { 0445 d->menu = nullptr; 0446 return; 0447 } 0448 0449 if (d->systemTrayIcon) { 0450 d->systemTrayIcon->setContextMenu(menu); 0451 } else if (d->menu != menu) { 0452 if (getenv("KSNI_NO_DBUSMENU")) { 0453 // This is a hack to make it possible to disable DBusMenu in an 0454 // application. The string "/NO_DBUSMENU" must be the same as in 0455 // DBusSystemTrayWidget::findDBusMenuInterface() in the Plasma 0456 // systemtray applet. 0457 d->menuObjectPath = QStringLiteral("/NO_DBUSMENU"); 0458 menu->installEventFilter(this); 0459 } else { 0460 d->menuObjectPath = QStringLiteral("/MenuBar"); 0461 #if HAVE_DBUSMENUQT 0462 new DBusMenuExporter(d->menuObjectPath, menu, d->statusNotifierItemDBus->dbusConnection()); 0463 Q_EMIT d->statusNotifierItemDBus->NewMenu(); 0464 #endif 0465 } 0466 0467 connect(menu, SIGNAL(aboutToShow()), this, SLOT(contextMenuAboutToShow())); 0468 } 0469 0470 d->menu = menu; 0471 Qt::WindowFlags oldFlags = d->menu->windowFlags(); 0472 d->menu->setParent(nullptr); 0473 d->menu->setWindowFlags(oldFlags); 0474 } 0475 0476 QMenu *KStatusNotifierItem::contextMenu() const 0477 { 0478 return d->menu; 0479 } 0480 0481 void KStatusNotifierItem::setAssociatedWidget(QWidget *associatedWidget) 0482 { 0483 if (associatedWidget) { 0484 d->associatedWidget = associatedWidget->window(); 0485 d->associatedWidgetPos = QPoint(-1, -1); 0486 0487 QObject::connect(KWindowSystem::self(), &KWindowSystem::windowAdded, d->associatedWidget, [this](WId id) { 0488 if (d->associatedWidget->winId() == id && d->associatedWidgetPos != QPoint(-1, -1)) { 0489 d->associatedWidget->move(d->associatedWidgetPos); 0490 } 0491 }); 0492 0493 QObject::connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, d->associatedWidget, [this](WId id) { 0494 if (d->associatedWidget->winId() == id) { 0495 d->associatedWidgetPos = d->associatedWidget->pos(); 0496 } 0497 }); 0498 } else if (d->associatedWidget) { 0499 KWindowSystem::self()->disconnect(d->associatedWidget); 0500 d->associatedWidget = nullptr; 0501 } 0502 0503 if (d->systemTrayIcon) { 0504 delete d->systemTrayIcon; 0505 d->systemTrayIcon = nullptr; 0506 d->setLegacySystemTrayEnabled(true); 0507 } 0508 0509 if (d->associatedWidget && d->associatedWidget != d->menu) { 0510 QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore")); 0511 0512 if (!action) { 0513 action = new QAction(this); 0514 d->actionCollection.insert(QStringLiteral("minimizeRestore"), action); 0515 action->setText(tr("&Minimize", "@action:inmenu")); 0516 connect(action, SIGNAL(triggered(bool)), this, SLOT(minimizeRestore())); 0517 } 0518 0519 KWindowInfo info(d->associatedWidget->winId(), NET::WMDesktop); 0520 d->onAllDesktops = info.onAllDesktops(); 0521 } else { 0522 if (d->menu && d->hasQuit) { 0523 QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore")); 0524 if (action) { 0525 d->menu->removeAction(action); 0526 } 0527 } 0528 0529 d->onAllDesktops = false; 0530 } 0531 } 0532 0533 QWidget *KStatusNotifierItem::associatedWidget() const 0534 { 0535 return d->associatedWidget; 0536 } 0537 0538 QList<QAction *> KStatusNotifierItem::actionCollection() const 0539 { 0540 return d->actionCollection.values(); 0541 } 0542 0543 void KStatusNotifierItem::addAction(const QString &name, QAction *action) 0544 { 0545 d->actionCollection.insert(name, action); 0546 } 0547 0548 void KStatusNotifierItem::removeAction(const QString &name) 0549 { 0550 d->actionCollection.remove(name); 0551 } 0552 0553 QAction *KStatusNotifierItem::action(const QString &name) const 0554 { 0555 return d->actionCollection.value(name); 0556 } 0557 0558 void KStatusNotifierItem::setStandardActionsEnabled(bool enabled) 0559 { 0560 if (d->standardActionsEnabled == enabled) { 0561 return; 0562 } 0563 0564 d->standardActionsEnabled = enabled; 0565 0566 if (d->menu && !enabled && d->hasQuit) { 0567 QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore")); 0568 if (action) { 0569 d->menu->removeAction(action); 0570 } 0571 0572 action = d->actionCollection.value(QStringLiteral("quit")); 0573 if (action) { 0574 d->menu->removeAction(action); 0575 } 0576 0577 d->hasQuit = false; 0578 } 0579 } 0580 0581 bool KStatusNotifierItem::standardActionsEnabled() const 0582 { 0583 return d->standardActionsEnabled; 0584 } 0585 0586 void KStatusNotifierItem::showMessage(const QString &title, const QString &message, const QString &icon, int timeout) 0587 { 0588 #ifdef QT_DBUS_LIB 0589 if (!d->notificationsClient) { 0590 d->notificationsClient = new org::freedesktop::Notifications(QStringLiteral("org.freedesktop.Notifications"), 0591 QStringLiteral("/org/freedesktop/Notifications"), 0592 QDBusConnection::sessionBus()); 0593 } 0594 0595 uint id = 0; 0596 QVariantMap hints; 0597 0598 QString desktopFileName = QGuiApplication::desktopFileName(); 0599 if (!desktopFileName.isEmpty()) { 0600 // handle apps which set the desktopFileName property with filename suffix, 0601 // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521) 0602 if (desktopFileName.endsWith(QLatin1String(".desktop"))) { 0603 desktopFileName.chop(8); 0604 } 0605 hints.insert(QStringLiteral("desktop-entry"), desktopFileName); 0606 } 0607 0608 d->notificationsClient->Notify(d->title, id, icon, title, message, QStringList(), hints, timeout); 0609 #else 0610 if (d->systemTrayIcon) { 0611 // Growl is not needed anymore for QSystemTrayIcon::showMessage() since OS X 10.8 0612 d->systemTrayIcon->showMessage(title, message, QSystemTrayIcon::Information, timeout); 0613 } 0614 #endif 0615 } 0616 0617 QString KStatusNotifierItem::title() const 0618 { 0619 return d->title; 0620 } 0621 0622 void KStatusNotifierItem::activate(const QPoint &pos) 0623 { 0624 // if the user activated the icon the NeedsAttention state is no longer necessary 0625 // FIXME: always true? 0626 if (d->status == NeedsAttention) { 0627 d->status = Active; 0628 #ifdef Q_OS_MACOS 0629 QtMac::setBadgeLabelText(QString()); 0630 #endif 0631 #ifdef QT_DBUS_LIB 0632 Q_EMIT d->statusNotifierItemDBus->NewStatus( 0633 QString::fromLatin1(metaObject()->enumerator(metaObject()->indexOfEnumerator("ItemStatus")).valueToKey(d->status))); 0634 #endif 0635 } 0636 0637 #ifdef QT_DBUS_LIB 0638 if (d->associatedWidget && d->associatedWidget == d->menu) { 0639 d->statusNotifierItemDBus->ContextMenu(pos.x(), pos.y()); 0640 return; 0641 } 0642 #endif 0643 0644 if (d->menu && d->menu->isVisible()) { 0645 d->menu->hide(); 0646 } 0647 0648 if (!d->associatedWidget) { 0649 Q_EMIT activateRequested(true, pos); 0650 return; 0651 } 0652 0653 d->checkVisibility(pos); 0654 } 0655 0656 void KStatusNotifierItem::hideAssociatedWidget() 0657 { 0658 if (!d->associatedWidget) { 0659 return; 0660 } 0661 d->minimizeRestore(false); 0662 } 0663 0664 QString KStatusNotifierItem::providedToken() const 0665 { 0666 #ifdef QT_DBUS_LIB 0667 return d->statusNotifierItemDBus->m_xdgActivationToken; 0668 #else 0669 return {}; 0670 #endif 0671 } 0672 0673 bool KStatusNotifierItemPrivate::checkVisibility(QPoint pos, bool perform) 0674 { 0675 // mapped = visible (but possibly obscured) 0676 const bool mapped = associatedWidget->isVisible() && !associatedWidget->isMinimized(); 0677 0678 // - not mapped -> show, raise, focus 0679 // - mapped 0680 // - obscured -> raise, focus 0681 // - not obscured -> hide 0682 // info1.mappingState() != NET::Visible -> window on another desktop? 0683 if (!mapped) { 0684 if (perform) { 0685 minimizeRestore(true); 0686 Q_EMIT q->activateRequested(true, pos); 0687 } 0688 0689 return true; 0690 } else if (QGuiApplication::platformName() == QLatin1String("xcb")) { 0691 const KWindowInfo info1(associatedWidget->winId(), NET::XAWMState | NET::WMState | NET::WMDesktop); 0692 QListIterator<WId> it(KWindowSystem::stackingOrder()); 0693 it.toBack(); 0694 while (it.hasPrevious()) { 0695 WId id = it.previous(); 0696 if (id == associatedWidget->winId()) { 0697 break; 0698 } 0699 0700 KWindowInfo info2(id, NET::WMDesktop | NET::WMGeometry | NET::XAWMState | NET::WMState | NET::WMWindowType); 0701 0702 if (info2.mappingState() != NET::Visible) { 0703 continue; // not visible on current desktop -> ignore 0704 } 0705 0706 if (!info2.geometry().intersects(associatedWidget->geometry())) { 0707 continue; // not obscuring the window -> ignore 0708 } 0709 0710 if (!info1.hasState(NET::KeepAbove) && info2.hasState(NET::KeepAbove)) { 0711 continue; // obscured by window kept above -> ignore 0712 } 0713 0714 /* clang-format off */ 0715 static constexpr auto flags = (NET::NormalMask 0716 | NET::DesktopMask 0717 | NET::DockMask 0718 | NET::ToolbarMask 0719 | NET::MenuMask 0720 | NET::DialogMask 0721 | NET::OverrideMask 0722 | NET::TopMenuMask 0723 | NET::UtilityMask 0724 | NET::SplashMask); 0725 /* clang-format on */ 0726 NET::WindowType type = info2.windowType(flags); 0727 0728 if (type == NET::Dock || type == NET::TopMenu) { 0729 continue; // obscured by dock or topmenu -> ignore 0730 } 0731 0732 if (perform) { 0733 KWindowSystem::raiseWindow(associatedWidget->winId()); 0734 KWindowSystem::forceActiveWindow(associatedWidget->winId()); 0735 Q_EMIT q->activateRequested(true, pos); 0736 } 0737 0738 return true; 0739 } 0740 0741 // not on current desktop? 0742 if (!info1.isOnCurrentDesktop()) { 0743 if (perform) { 0744 KWindowSystem::activateWindow(associatedWidget->winId()); 0745 Q_EMIT q->activateRequested(true, pos); 0746 } 0747 0748 return true; 0749 } 0750 0751 if (perform) { 0752 minimizeRestore(false); // hide 0753 Q_EMIT q->activateRequested(false, pos); 0754 } 0755 0756 return false; 0757 } else { 0758 if (perform) { 0759 minimizeRestore(false); // hide 0760 Q_EMIT q->activateRequested(false, pos); 0761 } 0762 return false; 0763 } 0764 0765 return true; 0766 } 0767 0768 bool KStatusNotifierItem::eventFilter(QObject *watched, QEvent *event) 0769 { 0770 if (d->systemTrayIcon == nullptr) { 0771 // FIXME: ugly ugly workaround to weird QMenu's focus problems 0772 if (watched == d->menu 0773 && (event->type() == QEvent::WindowDeactivate 0774 || (event->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton))) { 0775 // put at the back of even queue to let the action activate anyways 0776 QTimer::singleShot(0, this, [this]() { 0777 d->hideMenu(); 0778 }); 0779 } 0780 } 0781 return false; 0782 } 0783 0784 // KStatusNotifierItemPrivate 0785 0786 const int KStatusNotifierItemPrivate::s_protocolVersion = 0; 0787 0788 KStatusNotifierItemPrivate::KStatusNotifierItemPrivate(KStatusNotifierItem *item) 0789 : q(item) 0790 , category(KStatusNotifierItem::ApplicationStatus) 0791 , status(KStatusNotifierItem::Passive) 0792 , movie(nullptr) 0793 , systemTrayIcon(nullptr) 0794 , menu(nullptr) 0795 , associatedWidget(nullptr) 0796 , titleAction(nullptr) 0797 , hasQuit(false) 0798 , onAllDesktops(false) 0799 , standardActionsEnabled(true) 0800 { 0801 } 0802 0803 void KStatusNotifierItemPrivate::init(const QString &extraId) 0804 { 0805 q->setAssociatedWidget(qobject_cast<QWidget *>(q->parent())); 0806 #ifdef QT_DBUS_LIB 0807 qDBusRegisterMetaType<KDbusImageStruct>(); 0808 qDBusRegisterMetaType<KDbusImageVector>(); 0809 qDBusRegisterMetaType<KDbusToolTipStruct>(); 0810 0811 statusNotifierItemDBus = new KStatusNotifierItemDBus(q); 0812 0813 QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName), 0814 QDBusConnection::sessionBus(), 0815 QDBusServiceWatcher::WatchForOwnerChange, 0816 q); 0817 QObject::connect(watcher, SIGNAL(serviceOwnerChanged(QString, QString, QString)), q, SLOT(serviceChange(QString, QString, QString))); 0818 #endif 0819 0820 // create a default menu, just like in KSystemtrayIcon 0821 QMenu *m = new QMenu(associatedWidget); 0822 0823 title = QGuiApplication::applicationDisplayName(); 0824 if (title.isEmpty()) { 0825 title = QCoreApplication::applicationName(); 0826 } 0827 #ifdef Q_OS_MACOS 0828 // OS X doesn't have texted separators so we emulate QAction::addSection(): 0829 // we first add an action with the desired text (title) and icon 0830 titleAction = m->addAction(qApp->windowIcon(), title); 0831 // this action should be disabled 0832 titleAction->setEnabled(false); 0833 // Give the titleAction a visible menu icon: 0834 // Systray icon and menu ("menu extra") are often used by applications that provide no other interface. 0835 // It is thus reasonable to show the application icon in the menu; Finder, Dock and App Switcher 0836 // all show it in addition to the application name (and Apple's input "menu extra" also shows icons). 0837 titleAction->setIconVisibleInMenu(true); 0838 m->addAction(titleAction); 0839 // now add a regular separator 0840 m->addSeparator(); 0841 #else 0842 titleAction = m->addSection(qApp->windowIcon(), title); 0843 m->setTitle(title); 0844 #endif 0845 q->setContextMenu(m); 0846 0847 QAction *action = new QAction(q); 0848 action->setText(KStatusNotifierItem::tr("Quit", "@action:inmenu")); 0849 action->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); 0850 // cannot yet convert to function-pointer-based connect: 0851 // some apps like kalarm or korgac have a hack to rewire the connection 0852 // of the "quit" action to a own slot, and rely on the name-based slot to disconnect 0853 // TODO: extend KStatusNotifierItem API to support such needs 0854 QObject::connect(action, SIGNAL(triggered()), q, SLOT(maybeQuit())); 0855 actionCollection.insert(QStringLiteral("quit"), action); 0856 0857 id = title; 0858 if (!extraId.isEmpty()) { 0859 id.append(QLatin1Char('_')).append(extraId); 0860 } 0861 0862 // Init iconThemePath to the app folder for now 0863 iconThemePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, QStringLiteral("icons"), QStandardPaths::LocateDirectory); 0864 0865 registerToDaemon(); 0866 } 0867 0868 void KStatusNotifierItemPrivate::registerToDaemon() 0869 { 0870 bool useLegacy = false; 0871 #ifdef QT_DBUS_LIB 0872 qCDebug(LOG_KNOTIFICATIONS) << "Registering a client interface to the KStatusNotifierWatcher"; 0873 if (!statusNotifierWatcher) { 0874 statusNotifierWatcher = new org::kde::StatusNotifierWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName), 0875 QStringLiteral("/StatusNotifierWatcher"), 0876 QDBusConnection::sessionBus()); 0877 } 0878 0879 if (statusNotifierWatcher->isValid()) { 0880 // get protocol version in async way 0881 QDBusMessage msg = QDBusMessage::createMethodCall(QString::fromLatin1(s_statusNotifierWatcherServiceName), 0882 QStringLiteral("/StatusNotifierWatcher"), 0883 QStringLiteral("org.freedesktop.DBus.Properties"), 0884 QStringLiteral("Get")); 0885 msg.setArguments(QVariantList{QStringLiteral("org.kde.StatusNotifierWatcher"), QStringLiteral("ProtocolVersion")}); 0886 QDBusPendingCall async = QDBusConnection::sessionBus().asyncCall(msg); 0887 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, q); 0888 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, watcher] { 0889 watcher->deleteLater(); 0890 QDBusPendingReply<QVariant> reply = *watcher; 0891 if (reply.isError()) { 0892 qCDebug(LOG_KNOTIFICATIONS) << "Failed to read protocol version of KStatusNotifierWatcher"; 0893 setLegacySystemTrayEnabled(true); 0894 } else { 0895 bool ok = false; 0896 const int protocolVersion = reply.value().toInt(&ok); 0897 if (ok && protocolVersion == s_protocolVersion) { 0898 statusNotifierWatcher->RegisterStatusNotifierItem(statusNotifierItemDBus->service()); 0899 setLegacySystemTrayEnabled(false); 0900 } else { 0901 qCDebug(LOG_KNOTIFICATIONS) << "KStatusNotifierWatcher has incorrect protocol version"; 0902 setLegacySystemTrayEnabled(true); 0903 } 0904 } 0905 }); 0906 } else { 0907 qCDebug(LOG_KNOTIFICATIONS) << "KStatusNotifierWatcher not reachable"; 0908 useLegacy = true; 0909 } 0910 #else 0911 useLegacy = true; 0912 #endif 0913 setLegacySystemTrayEnabled(useLegacy); 0914 } 0915 0916 void KStatusNotifierItemPrivate::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner) 0917 { 0918 Q_UNUSED(name) 0919 if (newOwner.isEmpty()) { 0920 // unregistered 0921 qCDebug(LOG_KNOTIFICATIONS) << "Connection to the KStatusNotifierWatcher lost"; 0922 setLegacyMode(true); 0923 #ifdef QT_DBUS_LIB 0924 delete statusNotifierWatcher; 0925 statusNotifierWatcher = nullptr; 0926 #endif 0927 } else if (oldOwner.isEmpty()) { 0928 // registered 0929 setLegacyMode(false); 0930 } 0931 } 0932 0933 void KStatusNotifierItemPrivate::setLegacyMode(bool legacy) 0934 { 0935 if (legacy) { 0936 // unregistered 0937 setLegacySystemTrayEnabled(true); 0938 } else { 0939 // registered 0940 registerToDaemon(); 0941 } 0942 } 0943 0944 void KStatusNotifierItemPrivate::legacyWheelEvent(int delta) 0945 { 0946 #ifdef QT_DBUS_LIB 0947 statusNotifierItemDBus->Scroll(delta, QStringLiteral("vertical")); 0948 #endif 0949 } 0950 0951 void KStatusNotifierItemPrivate::legacyActivated(QSystemTrayIcon::ActivationReason reason) 0952 { 0953 if (reason == QSystemTrayIcon::MiddleClick) { 0954 Q_EMIT q->secondaryActivateRequested(systemTrayIcon->geometry().topLeft()); 0955 } else if (reason == QSystemTrayIcon::Trigger) { 0956 q->activate(systemTrayIcon->geometry().topLeft()); 0957 } 0958 } 0959 0960 void KStatusNotifierItemPrivate::setLegacySystemTrayEnabled(bool enabled) 0961 { 0962 if (enabled == (systemTrayIcon != nullptr)) { 0963 // already in the correct state 0964 return; 0965 } 0966 0967 if (enabled) { 0968 bool isKde = !qEnvironmentVariableIsEmpty("KDE_FULL_SESSION") || qgetenv("XDG_CURRENT_DESKTOP") == "KDE"; 0969 if (!systemTrayIcon && !isKde) { 0970 if (!QSystemTrayIcon::isSystemTrayAvailable()) { 0971 return; 0972 } 0973 systemTrayIcon = new KStatusNotifierLegacyIcon(associatedWidget); 0974 syncLegacySystemTrayIcon(); 0975 systemTrayIcon->setToolTip(toolTipTitle); 0976 systemTrayIcon->show(); 0977 QObject::connect(systemTrayIcon, SIGNAL(wheel(int)), q, SLOT(legacyWheelEvent(int))); 0978 QObject::connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), q, SLOT(legacyActivated(QSystemTrayIcon::ActivationReason))); 0979 } else if (isKde) { 0980 // prevent infinite recursion if the KDE platform plugin is loaded 0981 // but SNI is not available; see bug 350785 0982 qCWarning(LOG_KNOTIFICATIONS) << "env says KDE is running but SNI unavailable -- check " 0983 "KDE_FULL_SESSION and XDG_CURRENT_DESKTOP"; 0984 return; 0985 } 0986 0987 if (menu) { 0988 menu->setWindowFlags(Qt::Popup); 0989 } 0990 } else { 0991 delete systemTrayIcon; 0992 systemTrayIcon = nullptr; 0993 0994 if (menu) { 0995 menu->setWindowFlags(Qt::Window); 0996 } 0997 } 0998 0999 if (menu) { 1000 QMenu *m = menu; 1001 menu = nullptr; 1002 q->setContextMenu(m); 1003 } 1004 } 1005 1006 void KStatusNotifierItemPrivate::syncLegacySystemTrayIcon() 1007 { 1008 if (status == KStatusNotifierItem::NeedsAttention) { 1009 #ifdef Q_OS_MACOS 1010 QtMac::setBadgeLabelText(QString(QChar(0x26a0)) /*QStringLiteral("!")*/); 1011 if (attentionIconName.isNull() && attentionIcon.isNull()) { 1012 // code adapted from kmail's KMSystemTray::updateCount() 1013 int overlaySize = 22; 1014 QIcon attnIcon = qApp->windowIcon(); 1015 if (!attnIcon.availableSizes().isEmpty()) { 1016 overlaySize = attnIcon.availableSizes().at(0).width(); 1017 } 1018 QFont labelFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont); 1019 labelFont.setBold(true); 1020 QFontMetrics qfm(labelFont); 1021 float attnHeight = overlaySize * 0.667; 1022 if (qfm.height() > attnHeight) { 1023 float labelSize = attnHeight; 1024 labelFont.setPointSizeF(labelSize); 1025 } 1026 // Paint the label in a pixmap 1027 QPixmap overlayPixmap(overlaySize, overlaySize); 1028 overlayPixmap.fill(Qt::transparent); 1029 1030 QPainter p(&overlayPixmap); 1031 p.setFont(labelFont); 1032 p.setBrush(Qt::NoBrush); 1033 // this sort of badge/label is red on OS X 1034 p.setPen(QColor(224, 0, 0)); 1035 p.setOpacity(1.0); 1036 // use U+2022, the Unicode bullet 1037 p.drawText(overlayPixmap.rect(), Qt::AlignRight | Qt::AlignTop, QString(QChar(0x2022))); 1038 p.end(); 1039 1040 QPixmap iconPixmap = attnIcon.pixmap(overlaySize, overlaySize); 1041 QPainter pp(&iconPixmap); 1042 pp.drawPixmap(0, 0, overlayPixmap); 1043 pp.end(); 1044 systemTrayIcon->setIcon(iconPixmap); 1045 } else 1046 #endif 1047 { 1048 if (!movieName.isNull()) { 1049 if (!movie) { 1050 movie = new QMovie(movieName); 1051 } 1052 systemTrayIcon->setMovie(movie); 1053 } else if (!attentionIconName.isNull()) { 1054 systemTrayIcon->setIcon(QIcon::fromTheme(attentionIconName)); 1055 } else { 1056 systemTrayIcon->setIcon(attentionIcon); 1057 } 1058 } 1059 } else { 1060 #ifdef Q_OS_MACOS 1061 if (!iconName.isNull()) { 1062 QIcon theIcon = QIcon::fromTheme(iconName); 1063 systemTrayIcon->setIconWithMask(theIcon, status == KStatusNotifierItem::Passive); 1064 } else { 1065 systemTrayIcon->setIconWithMask(icon, status == KStatusNotifierItem::Passive); 1066 } 1067 QtMac::setBadgeLabelText(QString()); 1068 #else 1069 if (!iconName.isNull()) { 1070 systemTrayIcon->setIcon(QIcon::fromTheme(iconName)); 1071 } else { 1072 systemTrayIcon->setIcon(icon); 1073 } 1074 #endif 1075 } 1076 1077 systemTrayIcon->setToolTip(toolTipTitle); 1078 } 1079 1080 void KStatusNotifierItemPrivate::contextMenuAboutToShow() 1081 { 1082 if (!hasQuit && standardActionsEnabled) { 1083 // we need to add the actions to the menu afterwards so that these items 1084 // appear at the _END_ of the menu 1085 menu->addSeparator(); 1086 if (associatedWidget && associatedWidget != menu) { 1087 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore")); 1088 1089 if (action) { 1090 menu->addAction(action); 1091 } 1092 } 1093 1094 QAction *action = actionCollection.value(QStringLiteral("quit")); 1095 1096 if (action) { 1097 menu->addAction(action); 1098 } 1099 1100 hasQuit = true; 1101 } 1102 1103 if (associatedWidget && associatedWidget != menu) { 1104 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore")); 1105 if (checkVisibility(QPoint(0, 0), false)) { 1106 action->setText(KStatusNotifierItem::tr("&Restore", "@action:inmenu")); 1107 } else { 1108 action->setText(KStatusNotifierItem::tr("&Minimize", "@action:inmenu")); 1109 } 1110 } 1111 } 1112 1113 void KStatusNotifierItemPrivate::maybeQuit() 1114 { 1115 QString caption = QGuiApplication::applicationDisplayName(); 1116 if (caption.isEmpty()) { 1117 caption = QCoreApplication::applicationName(); 1118 } 1119 1120 const QString title = KStatusNotifierItem::tr("Confirm Quit From System Tray", "@title:window"); 1121 const QString query = KStatusNotifierItem::tr("<qt>Are you sure you want to quit <b>%1</b>?</qt>").arg(caption); 1122 1123 auto *dialog = new QMessageBox(QMessageBox::Question, title, query, QMessageBox::NoButton, associatedWidget); 1124 dialog->setAttribute(Qt::WA_DeleteOnClose); 1125 auto *quitButton = dialog->addButton(KStatusNotifierItem::tr("Quit", "@action:button"), QMessageBox::AcceptRole); 1126 quitButton->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); 1127 dialog->addButton(QMessageBox::Cancel); 1128 QObject::connect(dialog, &QDialog::accepted, qApp, &QApplication::quit); 1129 dialog->show(); 1130 } 1131 1132 void KStatusNotifierItemPrivate::minimizeRestore() 1133 { 1134 q->activate(systemTrayIcon ? systemTrayIcon->geometry().topLeft() : QPoint(0, 0)); 1135 } 1136 1137 void KStatusNotifierItemPrivate::hideMenu() 1138 { 1139 menu->hide(); 1140 } 1141 1142 void KStatusNotifierItemPrivate::minimizeRestore(bool show) 1143 { 1144 KWindowInfo info(associatedWidget->winId(), NET::WMDesktop); 1145 if (show) { 1146 if (onAllDesktops) { 1147 KWindowSystem::setOnAllDesktops(associatedWidget->winId(), true); 1148 } else { 1149 KWindowSystem::setCurrentDesktop(info.desktop()); 1150 } 1151 1152 auto state = associatedWidget->windowState() & ~Qt::WindowMinimized; 1153 associatedWidget->setWindowState(state); 1154 associatedWidget->show(); 1155 associatedWidget->raise(); 1156 if (associatedWidget->window()) { 1157 KWindowSystem::activateWindow(associatedWidget->window()->winId()); 1158 } 1159 } else { 1160 onAllDesktops = info.onAllDesktops(); 1161 associatedWidget->hide(); 1162 } 1163 } 1164 1165 #ifdef QT_DBUS_LIB 1166 KDbusImageStruct KStatusNotifierItemPrivate::imageToStruct(const QImage &image) 1167 { 1168 KDbusImageStruct icon; 1169 icon.width = image.size().width(); 1170 icon.height = image.size().height(); 1171 if (image.format() == QImage::Format_ARGB32) { 1172 icon.data = QByteArray((char *)image.bits(), image.sizeInBytes()); 1173 } else { 1174 QImage image32 = image.convertToFormat(QImage::Format_ARGB32); 1175 icon.data = QByteArray((char *)image32.bits(), image32.sizeInBytes()); 1176 } 1177 1178 // swap to network byte order if we are little endian 1179 if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { 1180 quint32 *uintBuf = (quint32 *)icon.data.data(); 1181 for (uint i = 0; i < icon.data.size() / sizeof(quint32); ++i) { 1182 *uintBuf = qToBigEndian(*uintBuf); 1183 ++uintBuf; 1184 } 1185 } 1186 1187 return icon; 1188 } 1189 1190 KDbusImageVector KStatusNotifierItemPrivate::iconToVector(const QIcon &icon) 1191 { 1192 KDbusImageVector iconVector; 1193 1194 QPixmap iconPixmap; 1195 1196 // if an icon exactly that size wasn't found don't add it to the vector 1197 const auto lstSizes = icon.availableSizes(); 1198 for (QSize size : lstSizes) { 1199 iconPixmap = icon.pixmap(size); 1200 iconVector.append(imageToStruct(iconPixmap.toImage())); 1201 } 1202 1203 return iconVector; 1204 } 1205 #endif 1206 1207 #include "moc_kstatusnotifieritem.cpp" 1208 #include "moc_kstatusnotifieritemprivate_p.cpp"