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