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"