KStatusNotifierItem

kstatusnotifieritem.cpp
1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2009 Marco Martin <notmart@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kstatusnotifieritem.h"
9#include "config-kstatusnotifieritem.h"
10#include "debug_p.h"
11#include "kstatusnotifieritemprivate_p.h"
12
13#include <QApplication>
14#include <QImage>
15#include <QMenu>
16#include <QMessageBox>
17#include <QMovie>
18#include <QPainter>
19#include <QPixmap>
20#include <QPushButton>
21#include <QStandardPaths>
22#ifdef Q_OS_MACOS
23#include <QFontDatabase>
24#endif
25
26#define slots
27#include <QtWidgets/private/qwidgetwindow_p.h>
28#undef slots
29
30#if HAVE_DBUS
31#include "kstatusnotifieritemdbus_p.h"
32
33#include <QDBusConnection>
34
35#if HAVE_DBUSMENUQT
36#include "libdbusmenu-qt/dbusmenuexporter.h"
37#endif // HAVE_DBUSMENUQT
38#endif
39
40#include <QTimer>
41#include <kwindowsystem.h>
42
43#if HAVE_X11
44#include <KWindowInfo>
45#include <KX11Extras>
46#endif
47
48#ifdef Q_OS_MACOS
49namespace MacUtils
50{
51void setBadgeLabelText(const QString &s);
52}
53#endif
54#include <cstdlib>
55
56static const char s_statusNotifierWatcherServiceName[] = "org.kde.StatusNotifierWatcher";
57static const int s_legacyTrayIconSize = 24;
58
61 , d(new KStatusNotifierItemPrivate(this))
62{
63 d->init(QString());
64}
65
68 , d(new KStatusNotifierItemPrivate(this))
69{
70 d->init(id);
71}
72
73KStatusNotifierItem::~KStatusNotifierItem()
74{
75#if HAVE_DBUS
76 delete d->statusNotifierWatcher;
77 delete d->notificationsClient;
78#endif
79 delete d->systemTrayIcon;
80 if (!qApp->closingDown()) {
81 delete d->menu;
82 }
83 if (d->associatedWindow) {
84 KWindowSystem::self()->disconnect(d->associatedWindow);
85 }
86}
87
89{
90 // qCDebug(LOG_KSTATUSNOTIFIERITEM) << "id requested" << d->id;
91 return d->id;
92}
93
95{
96 d->category = category;
97}
98
99KStatusNotifierItem::ItemStatus KStatusNotifierItem::status() const
100{
101 return d->status;
102}
103
104KStatusNotifierItem::ItemCategory KStatusNotifierItem::category() const
105{
106 return d->category;
107}
108
110{
111 d->title = title;
112}
113
115{
116 if (d->status == status) {
117 return;
118 }
119
120 d->status = status;
121
122#if HAVE_DBUS
123 Q_EMIT d->statusNotifierItemDBus->NewStatus(
124 QString::fromLatin1(metaObject()->enumerator(metaObject()->indexOfEnumerator("ItemStatus")).valueToKey(d->status)));
125#endif
126 if (d->systemTrayIcon) {
127 d->syncLegacySystemTrayIcon();
128 }
129}
130
131// normal icon
132
134{
135 if (d->iconName == name) {
136 return;
137 }
138
139 d->iconName = name;
140
141#if HAVE_DBUS
142 d->serializedIcon = KDbusImageVector();
143 Q_EMIT d->statusNotifierItemDBus->NewIcon();
144#endif
145
146 if (d->systemTrayIcon) {
147 d->systemTrayIcon->setIcon(QIcon::fromTheme(name));
148 }
149}
150
151QString KStatusNotifierItem::iconName() const
152{
153 return d->iconName;
154}
155
157{
158 if (d->iconName.isEmpty() && d->icon.cacheKey() == icon.cacheKey()) {
159 return;
160 }
161
162 d->iconName.clear();
163
164#if HAVE_DBUS
165 d->serializedIcon = d->iconToVector(icon);
166 Q_EMIT d->statusNotifierItemDBus->NewIcon();
167#endif
168
169 d->icon = icon;
170 if (d->systemTrayIcon) {
171 d->systemTrayIcon->setIcon(icon);
172 }
173}
174
176{
177 return d->icon;
178}
179
181{
182 if (d->overlayIconName == name) {
183 return;
184 }
185
186 d->overlayIconName = name;
187#if HAVE_DBUS
188 Q_EMIT d->statusNotifierItemDBus->NewOverlayIcon();
189#endif
190 if (d->systemTrayIcon) {
191 QPixmap iconPixmap = QIcon::fromTheme(d->iconName).pixmap(s_legacyTrayIconSize, s_legacyTrayIconSize);
192 if (!name.isEmpty()) {
193 QPixmap overlayPixmap = QIcon::fromTheme(d->overlayIconName).pixmap(s_legacyTrayIconSize / 2, s_legacyTrayIconSize / 2);
195 p.drawPixmap(iconPixmap.width() - overlayPixmap.width(), iconPixmap.height() - overlayPixmap.height(), overlayPixmap);
196 p.end();
197 }
198 d->systemTrayIcon->setIcon(iconPixmap);
199 }
200}
201
202QString KStatusNotifierItem::overlayIconName() const
203{
204 return d->overlayIconName;
205}
206
208{
209 if (d->overlayIconName.isEmpty() && d->overlayIcon.cacheKey() == icon.cacheKey()) {
210 return;
211 }
212
213 d->overlayIconName.clear();
214
215#if HAVE_DBUS
216 d->serializedOverlayIcon = d->iconToVector(icon);
217 Q_EMIT d->statusNotifierItemDBus->NewOverlayIcon();
218#endif
219
220 d->overlayIcon = icon;
221 if (d->systemTrayIcon) {
222 QPixmap iconPixmap = d->icon.pixmap(s_legacyTrayIconSize, s_legacyTrayIconSize);
223 QPixmap overlayPixmap = d->overlayIcon.pixmap(s_legacyTrayIconSize / 2, s_legacyTrayIconSize / 2);
224
226 p.drawPixmap(iconPixmap.width() - overlayPixmap.width(), iconPixmap.height() - overlayPixmap.height(), overlayPixmap);
227 p.end();
228 d->systemTrayIcon->setIcon(iconPixmap);
229 }
230}
231
233{
234 return d->overlayIcon;
235}
236
237// Icons and movie for requesting attention state
238
240{
241 if (d->attentionIconName == name) {
242 return;
243 }
244
245 d->attentionIconName = name;
246
247#if HAVE_DBUS
248 d->serializedAttentionIcon = KDbusImageVector();
249 Q_EMIT d->statusNotifierItemDBus->NewAttentionIcon();
250#endif
251}
252
253QString KStatusNotifierItem::attentionIconName() const
254{
255 return d->attentionIconName;
256}
257
259{
260 if (d->attentionIconName.isEmpty() && d->attentionIcon.cacheKey() == icon.cacheKey()) {
261 return;
262 }
263
264 d->attentionIconName.clear();
265 d->attentionIcon = icon;
266
267#if HAVE_DBUS
268 d->serializedAttentionIcon = d->iconToVector(icon);
269 Q_EMIT d->statusNotifierItemDBus->NewAttentionIcon();
270#endif
271}
272
274{
275 return d->attentionIcon;
276}
277
279{
280 if (d->movieName == name) {
281 return;
282 }
283
284 d->movieName = name;
285
286 delete d->movie;
287 d->movie = nullptr;
288
289#if HAVE_DBUS
290 Q_EMIT d->statusNotifierItemDBus->NewAttentionIcon();
291#endif
292
293 if (d->systemTrayIcon) {
294 d->movie = new QMovie(d->movieName);
295 d->systemTrayIcon->setMovie(d->movie);
296 }
297}
298
300{
301 return d->movieName;
302}
303
304// ToolTip
305
306#ifdef Q_OS_MACOS
307static void setTrayToolTip(KStatusNotifierLegacyIcon *systemTrayIcon, const QString &title, const QString &subTitle)
308{
309 if (systemTrayIcon) {
310 bool tEmpty = title.isEmpty(), stEmpty = subTitle.isEmpty();
311 if (tEmpty) {
312 if (!stEmpty) {
313 systemTrayIcon->setToolTip(subTitle);
314 } else {
315 systemTrayIcon->setToolTip(title);
316 }
317 } else {
318 if (stEmpty) {
319 systemTrayIcon->setToolTip(title);
320 } else {
321 systemTrayIcon->setToolTip(title + QStringLiteral("\n") + subTitle);
322 }
323 }
324 }
325}
326#else
327static void setTrayToolTip(KStatusNotifierLegacyIcon *systemTrayIcon, const QString &title, const QString &)
328{
329 if (systemTrayIcon) {
330 systemTrayIcon->setToolTip(title);
331 }
332}
333#endif
334
335void KStatusNotifierItem::setToolTip(const QString &iconName, const QString &title, const QString &subTitle)
336{
337 if (d->toolTipIconName == iconName && d->toolTipTitle == title && d->toolTipSubTitle == subTitle) {
338 return;
339 }
340
341 d->toolTipIconName = iconName;
342
343 d->toolTipTitle = title;
344 setTrayToolTip(d->systemTrayIcon, title, subTitle);
345 d->toolTipSubTitle = subTitle;
346
347#if HAVE_DBUS
348 d->serializedToolTipIcon = KDbusImageVector();
349 Q_EMIT d->statusNotifierItemDBus->NewToolTip();
350#endif
351}
352
353void KStatusNotifierItem::setToolTip(const QIcon &icon, const QString &title, const QString &subTitle)
354{
355 if (d->toolTipIconName.isEmpty() && d->toolTipIcon.cacheKey() == icon.cacheKey() //
356 && d->toolTipTitle == title //
357 && d->toolTipSubTitle == subTitle) {
358 return;
359 }
360
361 d->toolTipIconName.clear();
362 d->toolTipIcon = icon;
363
364 d->toolTipTitle = title;
365 setTrayToolTip(d->systemTrayIcon, title, subTitle);
366
367 d->toolTipSubTitle = subTitle;
368#if HAVE_DBUS
369 d->serializedToolTipIcon = d->iconToVector(icon);
370 Q_EMIT d->statusNotifierItemDBus->NewToolTip();
371#endif
372}
373
375{
376 if (d->toolTipIconName == name) {
377 return;
378 }
379
380 d->toolTipIconName = name;
381#if HAVE_DBUS
382 d->serializedToolTipIcon = KDbusImageVector();
383 Q_EMIT d->statusNotifierItemDBus->NewToolTip();
384#endif
385}
386
387QString KStatusNotifierItem::toolTipIconName() const
388{
389 return d->toolTipIconName;
390}
391
393{
394 if (d->toolTipIconName.isEmpty() && d->toolTipIcon.cacheKey() == icon.cacheKey()) {
395 return;
396 }
397
398 d->toolTipIconName.clear();
399 d->toolTipIcon = icon;
400
401#if HAVE_DBUS
402 d->serializedToolTipIcon = d->iconToVector(icon);
403 Q_EMIT d->statusNotifierItemDBus->NewToolTip();
404#endif
405}
406
408{
409 return d->toolTipIcon;
410}
411
413{
414 if (d->toolTipTitle == title) {
415 return;
416 }
417
418 d->toolTipTitle = title;
419
420#if HAVE_DBUS
421 Q_EMIT d->statusNotifierItemDBus->NewToolTip();
422#endif
423 setTrayToolTip(d->systemTrayIcon, title, d->toolTipSubTitle);
424}
425
426QString KStatusNotifierItem::toolTipTitle() const
427{
428 return d->toolTipTitle;
429}
430
432{
433 if (d->toolTipSubTitle == subTitle) {
434 return;
435 }
436
437 d->toolTipSubTitle = subTitle;
438#if HAVE_DBUS
439 Q_EMIT d->statusNotifierItemDBus->NewToolTip();
440#else
441 setTrayToolTip(d->systemTrayIcon, d->toolTipTitle, subTitle);
442#endif
443}
444
445QString KStatusNotifierItem::toolTipSubTitle() const
446{
447 return d->toolTipSubTitle;
448}
449
451{
452 if (d->menu && d->menu != menu) {
453 d->menu->removeEventFilter(this);
454 delete d->menu;
455 }
456
457 if (!menu) {
458 d->menu = nullptr;
459 return;
460 }
461
462 if (d->systemTrayIcon) {
463 d->systemTrayIcon->setContextMenu(menu);
464 } else if (d->menu != menu) {
465 if (getenv("KSNI_NO_DBUSMENU")) {
466 // This is a hack to make it possible to disable DBusMenu in an
467 // application. The string "/NO_DBUSMENU" must be the same as in
468 // DBusSystemTrayWidget::findDBusMenuInterface() in the Plasma
469 // systemtray applet.
470 d->menuObjectPath = QStringLiteral("/NO_DBUSMENU");
471 menu->installEventFilter(this);
472 } else {
473 d->menuObjectPath = QStringLiteral("/MenuBar");
474#if HAVE_DBUSMENUQT
475 new DBusMenuExporter(d->menuObjectPath, menu, d->statusNotifierItemDBus->dbusConnection());
476 Q_EMIT d->statusNotifierItemDBus->NewMenu();
477#endif
478 }
479
480 connect(menu, SIGNAL(aboutToShow()), this, SLOT(contextMenuAboutToShow()));
481 }
482
483 d->menu = menu;
484 Qt::WindowFlags oldFlags = d->menu->windowFlags();
485 d->menu->setParent(nullptr);
486 d->menu->setWindowFlags(oldFlags);
487}
488
490{
491 return d->menu;
492}
493
495{
496 if (associatedWindow) {
497 d->associatedWindow = associatedWindow;
498 d->associatedWindow->installEventFilter(this);
499 d->associatedWindowPos = QPoint(-1, -1);
500 } else {
501 if (d->associatedWindow) {
502 d->associatedWindow->removeEventFilter(this);
503 d->associatedWindow = nullptr;
504 }
505 }
506
507 if (d->systemTrayIcon) {
508 delete d->systemTrayIcon;
509 d->systemTrayIcon = nullptr;
510 d->setLegacySystemTrayEnabled(true);
511 }
512
513 if (d->associatedWindow) {
514 QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore"));
515
516 if (!action) {
517 action = new QAction(this);
518 d->actionCollection.insert(QStringLiteral("minimizeRestore"), action);
519 action->setText(tr("&Minimize", "@action:inmenu"));
520 action->setIcon(QIcon::fromTheme(QStringLiteral("window-minimize")));
521 connect(action, SIGNAL(triggered(bool)), this, SLOT(minimizeRestore()));
522 }
523
524#if HAVE_X11
526 KWindowInfo info(d->associatedWindow->winId(), NET::WMDesktop);
527 d->onAllDesktops = info.onAllDesktops();
528 }
529#endif
530 } else {
531 if (d->menu && d->hasQuit) {
532 QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore"));
533 if (action) {
534 d->menu->removeAction(action);
535 }
536 }
537
538 d->onAllDesktops = false;
539 }
540}
541
543{
544 return d->associatedWindow;
545}
546
547#if KSTATUSNOTIFIERITEM_BUILD_DEPRECATED_SINCE(6, 6)
548QList<QAction *> KStatusNotifierItem::actionCollection() const
549{
550 return d->actionCollection.values();
551}
552#endif
553
554#if KSTATUSNOTIFIERITEM_BUILD_DEPRECATED_SINCE(6, 6)
555void KStatusNotifierItem::addAction(const QString &name, QAction *action)
556{
557 d->actionCollection.insert(name, action);
558}
559#endif
560
561#if KSTATUSNOTIFIERITEM_BUILD_DEPRECATED_SINCE(6, 6)
562void KStatusNotifierItem::removeAction(const QString &name)
563{
564 d->actionCollection.remove(name);
565}
566#endif
567
568#if KSTATUSNOTIFIERITEM_BUILD_DEPRECATED_SINCE(6, 6)
569QAction *KStatusNotifierItem::action(const QString &name) const
570{
571 return d->actionCollection.value(name);
572}
573#endif
574
576{
577 if (d->standardActionsEnabled == enabled) {
578 return;
579 }
580
581 d->standardActionsEnabled = enabled;
582
583 if (d->menu && !enabled && d->hasQuit) {
584 QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore"));
585 if (action) {
586 d->menu->removeAction(action);
587 }
588
589 action = d->actionCollection.value(QStringLiteral("quit"));
590 if (action) {
591 d->menu->removeAction(action);
592 }
593
594 d->hasQuit = false;
595 }
596}
597
599{
600 return d->standardActionsEnabled;
601}
602
603void KStatusNotifierItem::showMessage(const QString &title, const QString &message, const QString &icon, int timeout)
604{
605#if HAVE_DBUS
606 if (!d->notificationsClient) {
607 d->notificationsClient = new org::freedesktop::Notifications(QStringLiteral("org.freedesktop.Notifications"),
608 QStringLiteral("/org/freedesktop/Notifications"),
610 }
611
612 uint id = 0;
613 QVariantMap hints;
614
615 QString desktopFileName = QGuiApplication::desktopFileName();
616 if (!desktopFileName.isEmpty()) {
617 // handle apps which set the desktopFileName property with filename suffix,
618 // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521)
619 if (desktopFileName.endsWith(QLatin1String(".desktop"))) {
620 desktopFileName.chop(8);
621 }
622 hints.insert(QStringLiteral("desktop-entry"), desktopFileName);
623 }
624
625 d->notificationsClient->Notify(d->title, id, icon, title, message, QStringList(), hints, timeout);
626#else
627 if (d->systemTrayIcon) {
628 // Growl is not needed anymore for QSystemTrayIcon::showMessage() since OS X 10.8
629 d->systemTrayIcon->showMessage(title, message, QSystemTrayIcon::Information, timeout);
630 }
631#endif
632}
633
634QString KStatusNotifierItem::title() const
635{
636 return d->title;
637}
638
640{
641 // if the user activated the icon the NeedsAttention state is no longer necessary
642 // FIXME: always true?
643 if (d->status == NeedsAttention) {
644 d->status = Active;
645#ifdef Q_OS_MACOS
646 MacUtils::setBadgeLabelText(QString());
647#endif
648#if HAVE_DBUS
649 Q_EMIT d->statusNotifierItemDBus->NewStatus(
650 QString::fromLatin1(metaObject()->enumerator(metaObject()->indexOfEnumerator("ItemStatus")).valueToKey(d->status)));
651#endif
652 }
653
654 if (d->menu && d->menu->isVisible()) {
655 d->menu->hide();
656 }
657
658 if (!d->associatedWindow) {
659 Q_EMIT activateRequested(true, pos);
660 return;
661 }
662
663 d->checkVisibility(pos);
664}
665
667{
668 if (!d->associatedWindow) {
669 return;
670 }
671 d->minimizeRestore(false);
672}
673
675{
676#if HAVE_DBUS
677 return d->statusNotifierItemDBus->m_xdgActivationToken;
678#else
679 return {};
680#endif
681}
682
684{
685 d->isMenu = isMenu;
686}
687
689{
690 return d->isMenu;
691}
692
693bool KStatusNotifierItemPrivate::checkVisibility(QPoint pos, bool perform)
694{
695 // mapped = visible (but possibly obscured)
696 const bool mapped = associatedWindow->isVisible() && !(associatedWindow->windowState() & Qt::WindowMinimized);
697
698 // - not mapped -> show, raise, focus
699 // - mapped
700 // - obscured -> raise, focus
701 // - not obscured -> hide
702 // info1.mappingState() != NET::Visible -> window on another desktop?
703 if (!mapped) {
704 if (perform) {
705 minimizeRestore(true);
706 Q_EMIT q->activateRequested(true, pos);
707 }
708
709 return true;
710#if HAVE_X11
711 } else if (QGuiApplication::platformName() == QLatin1String("xcb")) {
713 const KWindowInfo info1(associatedWindow->winId(), NET::XAWMState | NET::WMState | NET::WMDesktop);
715 it.toBack();
716 while (it.hasPrevious()) {
717 WId id = it.previous();
718 if (id == associatedWindow->winId()) {
719 break;
720 }
721
722 KWindowInfo info2(id, NET::WMDesktop | NET::WMGeometry | NET::XAWMState | NET::WMState | NET::WMWindowType);
723
724 if (info2.mappingState() != NET::Visible) {
725 continue; // not visible on current desktop -> ignore
726 }
727
728 if (!info2.geometry().intersects(associatedWindow->geometry())) {
729 continue; // not obscuring the window -> ignore
730 }
731
732 if (!info1.hasState(NET::KeepAbove) && info2.hasState(NET::KeepAbove)) {
733 continue; // obscured by window kept above -> ignore
734 }
735
736 /* clang-format off */
737 static constexpr auto flags = (NET::NormalMask
747 /* clang-format on */
748 NET::WindowType type = info2.windowType(flags);
749
750 if (type == NET::Dock || type == NET::TopMenu) {
751 continue; // obscured by dock or topmenu -> ignore
752 }
753
754 if (perform) {
755 KX11Extras::forceActiveWindow(associatedWindow->winId());
756 Q_EMIT q->activateRequested(true, pos);
757 }
758
759 return true;
760 }
761
762 // not on current desktop?
763 if (!info1.isOnCurrentDesktop()) {
764 if (perform) {
765 KWindowSystem::activateWindow(associatedWindow);
766 Q_EMIT q->activateRequested(true, pos);
767 }
768
769 return true;
770 }
771
772 if (perform) {
773 minimizeRestore(false); // hide
774 Q_EMIT q->activateRequested(false, pos);
775 }
776 }
777 return false;
778#endif
779 } else {
780 if (perform) {
781 if (!associatedWindow->isActive()) {
782 KWindowSystem::activateWindow(associatedWindow);
783 Q_EMIT q->activateRequested(true, pos);
784 } else {
785 minimizeRestore(false); // hide
786 Q_EMIT q->activateRequested(false, pos);
787 }
788 }
789 return false;
790 }
791
792 return true;
793}
794
795bool KStatusNotifierItem::eventFilter(QObject *watched, QEvent *event)
796{
797 if (watched == d->associatedWindow) {
798 if (event->type() == QEvent::Show) {
799 d->associatedWindow->setPosition(d->associatedWindowPos);
800 } else if (event->type() == QEvent::Hide) {
801 d->associatedWindowPos = d->associatedWindow->position();
802 }
803 }
804
805 if (d->systemTrayIcon == nullptr) {
806 // FIXME: ugly ugly workaround to weird QMenu's focus problems
807 if (watched == d->menu
808 && (event->type() == QEvent::WindowDeactivate
809 || (event->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton))) {
810 // put at the back of even queue to let the action activate anyways
811 QTimer::singleShot(0, this, [this]() {
812 d->hideMenu();
813 });
814 }
815 }
816 return false;
817}
818
819// KStatusNotifierItemPrivate
820
821const int KStatusNotifierItemPrivate::s_protocolVersion = 0;
822
823KStatusNotifierItemPrivate::KStatusNotifierItemPrivate(KStatusNotifierItem *item)
824 : q(item)
825 , category(KStatusNotifierItem::ApplicationStatus)
826 , status(KStatusNotifierItem::Passive)
827 , movie(nullptr)
828 , systemTrayIcon(nullptr)
829 , menu(nullptr)
830 , associatedWindow(nullptr)
831 , titleAction(nullptr)
832 , hasQuit(false)
833 , onAllDesktops(false)
834 , standardActionsEnabled(true)
835{
836}
837
838void KStatusNotifierItemPrivate::init(const QString &extraId)
839{
840 QWidget *parentWidget = qobject_cast<QWidget *>(q->parent());
841
842 q->setAssociatedWindow(parentWidget ? parentWidget->window()->windowHandle() : nullptr);
843#if HAVE_DBUS
844 qDBusRegisterMetaType<KDbusImageStruct>();
845 qDBusRegisterMetaType<KDbusImageVector>();
846 qDBusRegisterMetaType<KDbusToolTipStruct>();
847
848 statusNotifierItemDBus = new KStatusNotifierItemDBus(q);
849
850 QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName),
853 q);
854 QObject::connect(watcher, SIGNAL(serviceOwnerChanged(QString, QString, QString)), q, SLOT(serviceChange(QString, QString, QString)));
855#endif
856
857 // create a default menu, just like in KSystemtrayIcon
858 QMenu *m = new QMenu(parentWidget);
859
861 if (title.isEmpty()) {
863 }
864#ifdef Q_OS_MACOS
865 // OS X doesn't have texted separators so we emulate QAction::addSection():
866 // we first add an action with the desired text (title) and icon
867 titleAction = m->addAction(qApp->windowIcon(), title);
868 // this action should be disabled
869 titleAction->setEnabled(false);
870 // Give the titleAction a visible menu icon:
871 // Systray icon and menu ("menu extra") are often used by applications that provide no other interface.
872 // It is thus reasonable to show the application icon in the menu; Finder, Dock and App Switcher
873 // all show it in addition to the application name (and Apple's input "menu extra" also shows icons).
874 titleAction->setIconVisibleInMenu(true);
875 m->addAction(titleAction);
876 // now add a regular separator
877 m->addSeparator();
878#else
879 titleAction = m->addSection(qApp->windowIcon(), title);
880 m->setTitle(title);
881#endif
882 q->setContextMenu(m);
883
884 QAction *action = new QAction(q);
885 action->setText(KStatusNotifierItem::tr("Quit", "@action:inmenu"));
886 action->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
887 // cannot yet convert to function-pointer-based connect:
888 // some apps like kalarm or korgac have a hack to rewire the connection
889 // of the "quit" action to a own slot, and rely on the name-based slot to disconnect
890 // quitRequested/abortQuit was added for this use case
891 QObject::connect(action, SIGNAL(triggered()), q, SLOT(maybeQuit()));
892 actionCollection.insert(QStringLiteral("quit"), action);
893
894 id = title;
895 if (!extraId.isEmpty()) {
896 id.append(QLatin1Char('_')).append(extraId);
897 }
898
899 // Init iconThemePath to the app folder for now
901
902 registerToDaemon();
903}
904
905void KStatusNotifierItemPrivate::registerToDaemon()
906{
907 bool useLegacy = false;
908#if HAVE_DBUS
909 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Registering a client interface to the KStatusNotifierWatcher";
910 if (!statusNotifierWatcher) {
911 statusNotifierWatcher = new org::kde::StatusNotifierWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName),
912 QStringLiteral("/StatusNotifierWatcher"),
914 }
915
916 if (statusNotifierWatcher->isValid()) {
917 // get protocol version in async way
918 QDBusMessage msg = QDBusMessage::createMethodCall(QString::fromLatin1(s_statusNotifierWatcherServiceName),
919 QStringLiteral("/StatusNotifierWatcher"),
920 QStringLiteral("org.freedesktop.DBus.Properties"),
921 QStringLiteral("Get"));
922 msg.setArguments(QVariantList{QStringLiteral("org.kde.StatusNotifierWatcher"), QStringLiteral("ProtocolVersion")});
924 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, q);
925 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, watcher] {
926 watcher->deleteLater();
927 QDBusPendingReply<QVariant> reply = *watcher;
928 if (reply.isError()) {
929 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Failed to read protocol version of KStatusNotifierWatcher";
930 setLegacySystemTrayEnabled(true);
931 } else {
932 bool ok = false;
933 const int protocolVersion = reply.value().toInt(&ok);
934 if (ok && protocolVersion == s_protocolVersion) {
935 statusNotifierWatcher->RegisterStatusNotifierItem(statusNotifierItemDBus->service());
936 setLegacySystemTrayEnabled(false);
937 } else {
938 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "KStatusNotifierWatcher has incorrect protocol version";
939 setLegacySystemTrayEnabled(true);
940 }
941 }
942 });
943 } else {
944 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "KStatusNotifierWatcher not reachable";
945 useLegacy = true;
946 }
947#else
948 useLegacy = true;
949#endif
950 setLegacySystemTrayEnabled(useLegacy);
951}
952
953void KStatusNotifierItemPrivate::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner)
954{
955 Q_UNUSED(name)
956 if (newOwner.isEmpty()) {
957 // unregistered
958 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Connection to the KStatusNotifierWatcher lost";
959 setLegacyMode(true);
960#if HAVE_DBUS
961 delete statusNotifierWatcher;
962 statusNotifierWatcher = nullptr;
963#endif
964 } else if (oldOwner.isEmpty()) {
965 // registered
966 setLegacyMode(false);
967 }
968}
969
970void KStatusNotifierItemPrivate::setLegacyMode(bool legacy)
971{
972 if (legacy) {
973 // unregistered
974 setLegacySystemTrayEnabled(true);
975 } else {
976 // registered
977 registerToDaemon();
978 }
979}
980
981void KStatusNotifierItemPrivate::legacyWheelEvent(int delta)
982{
983#if HAVE_DBUS
984 statusNotifierItemDBus->Scroll(delta, QStringLiteral("vertical"));
985#endif
986}
987
988void KStatusNotifierItemPrivate::legacyActivated(QSystemTrayIcon::ActivationReason reason)
989{
990 if (reason == QSystemTrayIcon::MiddleClick) {
991 Q_EMIT q->secondaryActivateRequested(systemTrayIcon->geometry().topLeft());
992 } else if (reason == QSystemTrayIcon::Trigger) {
993 q->activate(systemTrayIcon->geometry().topLeft());
994 }
995}
996
997void KStatusNotifierItemPrivate::setLegacySystemTrayEnabled(bool enabled)
998{
999 if (enabled == (systemTrayIcon != nullptr)) {
1000 // already in the correct state
1001 return;
1002 }
1003
1004 if (enabled) {
1005 bool isKde = !qEnvironmentVariableIsEmpty("KDE_FULL_SESSION")
1006 || qgetenv("XDG_CURRENT_DESKTOP") == "KDE"
1007 || qgetenv("QT_QPA_PLATFORMTHEME").toLower() == "kde";
1008 if (!systemTrayIcon && !isKde) {
1010 return;
1011 }
1012 systemTrayIcon = new KStatusNotifierLegacyIcon(q);
1013 syncLegacySystemTrayIcon();
1014 systemTrayIcon->setToolTip(toolTipTitle);
1015 systemTrayIcon->show();
1016 QObject::connect(systemTrayIcon, SIGNAL(wheel(int)), q, SLOT(legacyWheelEvent(int)));
1017 QObject::connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), q, SLOT(legacyActivated(QSystemTrayIcon::ActivationReason)));
1018 } else if (isKde) {
1019 // prevent infinite recursion if the KDE platform plugin is loaded
1020 // but SNI is not available; see bug 350785
1021 qCWarning(LOG_KSTATUSNOTIFIERITEM) << "KDE platform plugin is loaded but SNI unavailable";
1022 return;
1023 }
1024
1025 if (menu) {
1026 menu->setWindowFlags(Qt::Popup);
1027 }
1028 } else {
1029 delete systemTrayIcon;
1030 systemTrayIcon = nullptr;
1031
1032 if (menu) {
1033 menu->setWindowFlags(Qt::Window);
1034 }
1035 }
1036
1037 if (menu) {
1038 QMenu *m = menu;
1039 menu = nullptr;
1040 q->setContextMenu(m);
1041 }
1042}
1043
1044void KStatusNotifierItemPrivate::syncLegacySystemTrayIcon()
1045{
1047#ifdef Q_OS_MACOS
1048 MacUtils::setBadgeLabelText(QString(QChar(0x26a0)) /*QStringLiteral("!")*/);
1049 if (attentionIconName.isNull() && attentionIcon.isNull()) {
1050 // code adapted from kmail's KMSystemTray::updateCount()
1051 int overlaySize = 22;
1052 QIcon attnIcon = qApp->windowIcon();
1053 if (!attnIcon.availableSizes().isEmpty()) {
1054 overlaySize = attnIcon.availableSizes().at(0).width();
1055 }
1057 labelFont.setBold(true);
1058 QFontMetrics qfm(labelFont);
1059 float attnHeight = overlaySize * 0.667;
1060 if (qfm.height() > attnHeight) {
1061 float labelSize = attnHeight;
1062 labelFont.setPointSizeF(labelSize);
1063 }
1064 // Paint the label in a pixmap
1065 QPixmap overlayPixmap(overlaySize, overlaySize);
1066 overlayPixmap.fill(Qt::transparent);
1067
1068 QPainter p(&overlayPixmap);
1069 p.setFont(labelFont);
1070 p.setBrush(Qt::NoBrush);
1071 // this sort of badge/label is red on OS X
1072 p.setPen(QColor(224, 0, 0));
1073 p.setOpacity(1.0);
1074 // use U+2022, the Unicode bullet
1075 p.drawText(overlayPixmap.rect(), Qt::AlignRight | Qt::AlignTop, QString(QChar(0x2022)));
1076 p.end();
1077
1078 QPixmap iconPixmap = attnIcon.pixmap(overlaySize, overlaySize);
1079 QPainter pp(&iconPixmap);
1080 pp.drawPixmap(0, 0, overlayPixmap);
1081 pp.end();
1082 systemTrayIcon->setIcon(iconPixmap);
1083 } else
1084#endif
1085 {
1086 if (!movieName.isNull()) {
1087 if (!movie) {
1088 movie = new QMovie(movieName);
1089 }
1090 systemTrayIcon->setMovie(movie);
1091 } else if (!attentionIconName.isNull()) {
1092 systemTrayIcon->setIcon(QIcon::fromTheme(attentionIconName));
1093 } else {
1094 systemTrayIcon->setIcon(attentionIcon);
1095 }
1096 }
1097 } else {
1098#ifdef Q_OS_MACOS
1099 if (!iconName.isNull()) {
1100 QIcon theIcon = QIcon::fromTheme(iconName);
1101 systemTrayIcon->setIconWithMask(theIcon, status == KStatusNotifierItem::Passive);
1102 } else {
1103 systemTrayIcon->setIconWithMask(icon, status == KStatusNotifierItem::Passive);
1104 }
1105 MacUtils::setBadgeLabelText(QString());
1106#else
1107 if (!iconName.isNull()) {
1108 systemTrayIcon->setIcon(QIcon::fromTheme(iconName));
1109 } else {
1110 systemTrayIcon->setIcon(icon);
1111 }
1112#endif
1113 }
1114
1115 systemTrayIcon->setToolTip(toolTipTitle);
1116}
1117
1118void KStatusNotifierItemPrivate::contextMenuAboutToShow()
1119{
1120 if (!hasQuit && standardActionsEnabled) {
1121 // we need to add the actions to the menu afterwards so that these items
1122 // appear at the _END_ of the menu
1123 menu->addSeparator();
1124 if (associatedWindow) {
1125 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore"));
1126
1127 if (action) {
1128 menu->addAction(action);
1129 }
1130 }
1131
1132 QAction *action = actionCollection.value(QStringLiteral("quit"));
1133
1134 if (action) {
1135 menu->addAction(action);
1136 }
1137
1138 hasQuit = true;
1139 }
1140
1141 if (associatedWindow) {
1142 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore"));
1143 if (checkVisibility(QPoint(0, 0), false)) {
1144 action->setText(KStatusNotifierItem::tr("&Restore", "@action:inmenu"));
1145 action->setIcon(QIcon::fromTheme(QStringLiteral("window-restore")));
1146 } else {
1147 action->setText(KStatusNotifierItem::tr("&Minimize", "@action:inmenu"));
1148 action->setIcon(QIcon::fromTheme(QStringLiteral("window-minimize")));
1149 }
1150 }
1151}
1152
1154{
1155 d->quitAborted = true;
1156}
1157
1158void KStatusNotifierItemPrivate::maybeQuit()
1159{
1160 Q_EMIT q->quitRequested();
1161
1162 if (quitAborted) {
1163 quitAborted = false;
1164 return;
1165 }
1166
1168 if (caption.isEmpty()) {
1170 }
1171
1172 const QString title = KStatusNotifierItem::tr("Confirm Quit From System Tray", "@title:window");
1173 const QString query = KStatusNotifierItem::tr("<qt>Are you sure you want to quit <b>%1</b>?</qt>").arg(caption);
1174
1175 auto *dialog = new QMessageBox(QMessageBox::Question, title, query, QMessageBox::NoButton);
1176 dialog->setAttribute(Qt::WA_DeleteOnClose);
1177
1178 auto *quitButton = dialog->addButton(KStatusNotifierItem::tr("Quit", "@action:button"), QMessageBox::AcceptRole);
1179 quitButton->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
1180 dialog->addButton(QMessageBox::Cancel);
1182 dialog->show();
1183 dialog->windowHandle()->setTransientParent(associatedWindow);
1184}
1185
1186void KStatusNotifierItemPrivate::minimizeRestore()
1187{
1188 q->activate(systemTrayIcon ? systemTrayIcon->geometry().topLeft() : QPoint(0, 0));
1189}
1190
1191void KStatusNotifierItemPrivate::hideMenu()
1192{
1193 menu->hide();
1194}
1195
1196void KStatusNotifierItemPrivate::minimizeRestore(bool show)
1197{
1198#if HAVE_X11
1200 KWindowInfo info(associatedWindow->winId(), NET::WMDesktop);
1201
1202 if (show) {
1203 if (onAllDesktops) {
1204 KX11Extras::setOnAllDesktops(associatedWindow->winId(), true);
1205 } else {
1206 KX11Extras::setCurrentDesktop(info.desktop());
1207 }
1208 } else {
1209 onAllDesktops = info.onAllDesktops();
1210 }
1211 }
1212#endif
1213
1214 if (show) {
1215 Qt::WindowState state = (Qt::WindowState)(associatedWindow->windowState() & ~Qt::WindowMinimized);
1216 associatedWindow->setWindowState(state);
1217 // Work around https://bugreports.qt.io/browse/QTBUG-120316
1218 if (auto *widgetwindow = static_cast<QWidgetWindow*>(associatedWindow->qt_metacast("QWidgetWindow"))) {
1219 widgetwindow->widget()->show();
1220 } else {
1221 associatedWindow->show();
1222 }
1223 associatedWindow->raise();
1224 KWindowSystem::activateWindow(associatedWindow);
1225 } else {
1226 // Work around https://bugreports.qt.io/browse/QTBUG-120316
1227 if (auto *widgetwindow = static_cast<QWidgetWindow*>(associatedWindow->qt_metacast("QWidgetWindow"))) {
1228 widgetwindow->widget()->hide();
1229 } else {
1230 associatedWindow->hide();
1231 }
1232 }
1233}
1234
1235#if HAVE_DBUS
1236KDbusImageStruct KStatusNotifierItemPrivate::imageToStruct(const QImage &image)
1237{
1238 KDbusImageStruct icon;
1239 icon.width = image.size().width();
1240 icon.height = image.size().height();
1241 if (image.format() == QImage::Format_ARGB32) {
1242 icon.data = QByteArray((char *)image.bits(), image.sizeInBytes());
1243 } else {
1245 icon.data = QByteArray((char *)image32.bits(), image32.sizeInBytes());
1246 }
1247
1248 // swap to network byte order if we are little endian
1250 quint32 *uintBuf = (quint32 *)icon.data.data();
1251 for (uint i = 0; i < icon.data.size() / sizeof(quint32); ++i) {
1252 *uintBuf = qToBigEndian(*uintBuf);
1253 ++uintBuf;
1254 }
1255 }
1256
1257 return icon;
1258}
1259
1260KDbusImageVector KStatusNotifierItemPrivate::iconToVector(const QIcon &icon)
1261{
1262 KDbusImageVector iconVector;
1263
1264 QPixmap iconPixmap;
1265
1266 // if an icon exactly that size wasn't found don't add it to the vector
1267 auto lstSizes = icon.availableSizes();
1268 if (lstSizes.isEmpty() && !icon.isNull()) {
1269 // if the icon is a svg icon, then available Sizes will be empty, try some common sizes
1270 lstSizes = {{16, 16}, {22, 22}, {32, 32}};
1271 }
1272 for (QSize size : lstSizes) {
1273 iconPixmap = icon.pixmap(size);
1274 if (!iconPixmap.isNull()) {
1275 iconVector.append(imageToStruct(iconPixmap.toImage()));
1276 }
1277 }
1278 return iconVector;
1279}
1280#endif
1281
1282#include "moc_kstatusnotifieritem.cpp"
1283#include "moc_kstatusnotifieritemprivate_p.cpp"
A DBusMenuExporter instance can serialize a menu over DBus.
KDE Status notifier Item protocol implementation
virtual void activate(const QPoint &pos=QPoint())
Shows the main window and try to position it on top of the other windows, if the window is already vi...
void setStatus(const ItemStatus status)
Sets a new status for this icon.
void setAssociatedWindow(QWindow *window)
Sets the main window associated with this StatusNotifierItem.
void setContextMenu(QMenu *menu)
Sets a new context menu for this StatusNotifierItem.
ItemCategory
Different kinds of applications announce their type to the systemtray, so can be drawn in a different...
QWindow * associatedWindow() const
Access the main window associated with this StatusNotifierItem.
void setAttentionIconByName(const QString &name)
Sets a new icon that should be used when the application wants to request attention (usually the syst...
void activateRequested(bool active, const QPoint &pos)
Inform the host application that an activation has been requested, for instance left mouse click,...
void setToolTipIconByName(const QString &name)
Set a new icon for the toolTip.
void abortQuit()
Cancelles an ongoing quit operation.
void setToolTipSubTitle(const QString &subTitle)
Sets a new subtitle for the toolTip.
ItemStatus
All the possible status this icon can have, depending on the importance of the events that happens in...
@ Active
The application is doing something, or it is important that the icon is always reachable from the use...
@ NeedsAttention
The application requests the attention of the user, for instance battery running out or a new IM mess...
@ Passive
Nothing is happening in the application, so showing this icon is not required. This is the default va...
void setIsMenu(bool isMenu)
Indictates that this item only supports the context menu.
void setToolTip(const QString &iconName, const QString &title, const QString &subTitle)
Sets a new toolTip or this icon, a toolTip is composed of an icon, a title and a text,...
void setStandardActionsEnabled(bool enabled)
Sets whether to show the standard items in the menu, such as Quit.
void setOverlayIconByPixmap(const QIcon &icon)
Sets an icon to be used as overlay for the main one setOverlayIconByPixmap(QIcon()) will remove the o...
void showMessage(const QString &title, const QString &message, const QString &icon, int timeout=10000)
Shows the user a notification.
void setIconByName(const QString &name)
Sets a new main icon for the system tray.
void hideAssociatedWindow()
Hides the main window, if not already hidden.
void setIconByPixmap(const QIcon &icon)
Sets a new main icon for the system tray.
QString attentionMovieName() const
void setCategory(const ItemCategory category)
Sets the category for this icon, usually it's needed to call this function only once.
void setAttentionMovieByName(const QString &name)
Sets a movie as the requesting attention icon.
void setTitle(const QString &title)
Sets a title for this icon.
void setAttentionIconByPixmap(const QIcon &icon)
Sets the pixmap of the requesting attention icon.
KStatusNotifierItem(QObject *parent=nullptr)
Construct a new status notifier item.
QMenu * contextMenu() const
Access the context menu associated to this status notifier item.
void setToolTipTitle(const QString &title)
Sets a new title for the toolTip.
void setOverlayIconByName(const QString &name)
Sets an icon to be used as overlay for the main one.
void setToolTipIconByPixmap(const QIcon &icon)
Set a new icon for the toolTip.
bool onAllDesktops() const
static Q_INVOKABLE void activateWindow(QWindow *window, long time=0)
static bool isPlatformX11()
static KWindowSystem * self()
static void setOnAllDesktops(WId win, bool b)
static void setCurrentDesktop(int desktop)
static QList< WId > stackingOrder()
static Q_INVOKABLE void forceActiveWindow(QWindow *window, long time=0)
DialogMask
SplashMask
UtilityMask
OverrideMask
ToolbarMask
NormalMask
DesktopMask
TopMenuMask
WindowType
Q_SCRIPTABLE CaptureState status()
Type type(const QSqlDatabase &db)
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
Category category(StandardShortcut id)
void setEnabled(bool)
void setIcon(const QIcon &icon)
void setText(const QString &text)
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
QDBusConnection sessionBus()
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
void setArguments(const QList< QVariant > &arguments)
void finished(QDBusPendingCallWatcher *self)
bool isError() const const
typename Select< 0 >::Type value() const const
void accepted()
void setBold(bool enable)
void setPointSizeF(qreal pointSize)
QFont systemFont(SystemFont type)
QPixmap pixmap(QWindow *window, const QSize &size, Mode mode, State state) const const
QList< QSize > availableSizes(Mode mode, State state) const const
qint64 cacheKey() const const
QIcon fromTheme(const QString &name)
bool isNull() const const
uchar * bits()
QImage convertToFormat(Format format, Qt::ImageConversionFlags flags) &&
Format format() const const
QSize size() const const
qsizetype sizeInBytes() const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addSection(const QIcon &icon, const QString &text)
QAction * addSeparator()
void setTitle(const QString &title)
QObject(QObject *parent)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
bool disconnect(const QMetaObject::Connection &connection)
virtual bool event(QEvent *e)
void installEventFilter(QObject *filterObj)
virtual const QMetaObject * metaObject() const const
QObject * parent() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
void drawPixmap(const QPoint &point, const QPixmap &pixmap)
bool end()
int height() const const
bool isNull() const const
QImage toImage() const const
int width() const const
int height() const const
int width() const const
QString locate(StandardLocation type, const QString &fileName, LocateOptions options)
QString arg(Args &&... args) const const
void chop(qsizetype n)
void clear()
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
bool isSystemTrayAvailable()
AlignRight
transparent
LeftButton
WA_DeleteOnClose
WindowMinimized
typedef WindowFlags
QWidget * window() const const
QWindow * windowHandle() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Apr 11 2025 11:58:18 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.