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
683bool KStatusNotifierItemPrivate::checkVisibility(QPoint pos, bool perform)
684{
685 // mapped = visible (but possibly obscured)
686 const bool mapped = associatedWindow->isVisible() && !(associatedWindow->windowState() & Qt::WindowMinimized);
687
688 // - not mapped -> show, raise, focus
689 // - mapped
690 // - obscured -> raise, focus
691 // - not obscured -> hide
692 // info1.mappingState() != NET::Visible -> window on another desktop?
693 if (!mapped) {
694 if (perform) {
695 minimizeRestore(true);
696 Q_EMIT q->activateRequested(true, pos);
697 }
698
699 return true;
700#if HAVE_X11
701 } else if (QGuiApplication::platformName() == QLatin1String("xcb")) {
703 const KWindowInfo info1(associatedWindow->winId(), NET::XAWMState | NET::WMState | NET::WMDesktop);
705 it.toBack();
706 while (it.hasPrevious()) {
707 WId id = it.previous();
708 if (id == associatedWindow->winId()) {
709 break;
710 }
711
712 KWindowInfo info2(id, NET::WMDesktop | NET::WMGeometry | NET::XAWMState | NET::WMState | NET::WMWindowType);
713
714 if (info2.mappingState() != NET::Visible) {
715 continue; // not visible on current desktop -> ignore
716 }
717
718 if (!info2.geometry().intersects(associatedWindow->geometry())) {
719 continue; // not obscuring the window -> ignore
720 }
721
722 if (!info1.hasState(NET::KeepAbove) && info2.hasState(NET::KeepAbove)) {
723 continue; // obscured by window kept above -> ignore
724 }
725
726 /* clang-format off */
727 static constexpr auto flags = (NET::NormalMask
737 /* clang-format on */
738 NET::WindowType type = info2.windowType(flags);
739
740 if (type == NET::Dock || type == NET::TopMenu) {
741 continue; // obscured by dock or topmenu -> ignore
742 }
743
744 if (perform) {
745 KX11Extras::forceActiveWindow(associatedWindow->winId());
746 Q_EMIT q->activateRequested(true, pos);
747 }
748
749 return true;
750 }
751
752 // not on current desktop?
753 if (!info1.isOnCurrentDesktop()) {
754 if (perform) {
755 KWindowSystem::activateWindow(associatedWindow);
756 Q_EMIT q->activateRequested(true, pos);
757 }
758
759 return true;
760 }
761
762 if (perform) {
763 minimizeRestore(false); // hide
764 Q_EMIT q->activateRequested(false, pos);
765 }
766 }
767 return false;
768#endif
769 } else {
770 if (perform) {
771 if (!associatedWindow->isActive()) {
772 KWindowSystem::activateWindow(associatedWindow);
773 Q_EMIT q->activateRequested(true, pos);
774 } else {
775 minimizeRestore(false); // hide
776 Q_EMIT q->activateRequested(false, pos);
777 }
778 }
779 return false;
780 }
781
782 return true;
783}
784
785bool KStatusNotifierItem::eventFilter(QObject *watched, QEvent *event)
786{
787 if (watched == d->associatedWindow) {
788 if (event->type() == QEvent::Show) {
789 d->associatedWindow->setPosition(d->associatedWindowPos);
790 } else if (event->type() == QEvent::Hide) {
791 d->associatedWindowPos = d->associatedWindow->position();
792 }
793 }
794
795 if (d->systemTrayIcon == nullptr) {
796 // FIXME: ugly ugly workaround to weird QMenu's focus problems
797 if (watched == d->menu
798 && (event->type() == QEvent::WindowDeactivate
799 || (event->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton))) {
800 // put at the back of even queue to let the action activate anyways
801 QTimer::singleShot(0, this, [this]() {
802 d->hideMenu();
803 });
804 }
805 }
806 return false;
807}
808
809// KStatusNotifierItemPrivate
810
811const int KStatusNotifierItemPrivate::s_protocolVersion = 0;
812
813KStatusNotifierItemPrivate::KStatusNotifierItemPrivate(KStatusNotifierItem *item)
814 : q(item)
815 , category(KStatusNotifierItem::ApplicationStatus)
816 , status(KStatusNotifierItem::Passive)
817 , movie(nullptr)
818 , systemTrayIcon(nullptr)
819 , menu(nullptr)
820 , associatedWindow(nullptr)
821 , titleAction(nullptr)
822 , hasQuit(false)
823 , onAllDesktops(false)
824 , standardActionsEnabled(true)
825{
826}
827
828void KStatusNotifierItemPrivate::init(const QString &extraId)
829{
830 QWidget *parentWidget = qobject_cast<QWidget *>(q->parent());
831
832 q->setAssociatedWindow(parentWidget ? parentWidget->window()->windowHandle() : nullptr);
833#if HAVE_DBUS
834 qDBusRegisterMetaType<KDbusImageStruct>();
835 qDBusRegisterMetaType<KDbusImageVector>();
836 qDBusRegisterMetaType<KDbusToolTipStruct>();
837
838 statusNotifierItemDBus = new KStatusNotifierItemDBus(q);
839
840 QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName),
843 q);
844 QObject::connect(watcher, SIGNAL(serviceOwnerChanged(QString, QString, QString)), q, SLOT(serviceChange(QString, QString, QString)));
845#endif
846
847 // create a default menu, just like in KSystemtrayIcon
848 QMenu *m = new QMenu(parentWidget);
849
851 if (title.isEmpty()) {
853 }
854#ifdef Q_OS_MACOS
855 // OS X doesn't have texted separators so we emulate QAction::addSection():
856 // we first add an action with the desired text (title) and icon
857 titleAction = m->addAction(qApp->windowIcon(), title);
858 // this action should be disabled
859 titleAction->setEnabled(false);
860 // Give the titleAction a visible menu icon:
861 // Systray icon and menu ("menu extra") are often used by applications that provide no other interface.
862 // It is thus reasonable to show the application icon in the menu; Finder, Dock and App Switcher
863 // all show it in addition to the application name (and Apple's input "menu extra" also shows icons).
864 titleAction->setIconVisibleInMenu(true);
865 m->addAction(titleAction);
866 // now add a regular separator
867 m->addSeparator();
868#else
869 titleAction = m->addSection(qApp->windowIcon(), title);
870 m->setTitle(title);
871#endif
872 q->setContextMenu(m);
873
874 QAction *action = new QAction(q);
875 action->setText(KStatusNotifierItem::tr("Quit", "@action:inmenu"));
876 action->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
877 // cannot yet convert to function-pointer-based connect:
878 // some apps like kalarm or korgac have a hack to rewire the connection
879 // of the "quit" action to a own slot, and rely on the name-based slot to disconnect
880 // quitRequested/abortQuit was added for this use case
881 QObject::connect(action, SIGNAL(triggered()), q, SLOT(maybeQuit()));
882 actionCollection.insert(QStringLiteral("quit"), action);
883
884 id = title;
885 if (!extraId.isEmpty()) {
886 id.append(QLatin1Char('_')).append(extraId);
887 }
888
889 // Init iconThemePath to the app folder for now
891
892 registerToDaemon();
893}
894
895void KStatusNotifierItemPrivate::registerToDaemon()
896{
897 bool useLegacy = false;
898#if HAVE_DBUS
899 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Registering a client interface to the KStatusNotifierWatcher";
900 if (!statusNotifierWatcher) {
901 statusNotifierWatcher = new org::kde::StatusNotifierWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName),
902 QStringLiteral("/StatusNotifierWatcher"),
904 }
905
906 if (statusNotifierWatcher->isValid()) {
907 // get protocol version in async way
908 QDBusMessage msg = QDBusMessage::createMethodCall(QString::fromLatin1(s_statusNotifierWatcherServiceName),
909 QStringLiteral("/StatusNotifierWatcher"),
910 QStringLiteral("org.freedesktop.DBus.Properties"),
911 QStringLiteral("Get"));
912 msg.setArguments(QVariantList{QStringLiteral("org.kde.StatusNotifierWatcher"), QStringLiteral("ProtocolVersion")});
914 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, q);
915 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, watcher] {
916 watcher->deleteLater();
917 QDBusPendingReply<QVariant> reply = *watcher;
918 if (reply.isError()) {
919 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Failed to read protocol version of KStatusNotifierWatcher";
920 setLegacySystemTrayEnabled(true);
921 } else {
922 bool ok = false;
923 const int protocolVersion = reply.value().toInt(&ok);
924 if (ok && protocolVersion == s_protocolVersion) {
925 statusNotifierWatcher->RegisterStatusNotifierItem(statusNotifierItemDBus->service());
926 setLegacySystemTrayEnabled(false);
927 } else {
928 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "KStatusNotifierWatcher has incorrect protocol version";
929 setLegacySystemTrayEnabled(true);
930 }
931 }
932 });
933 } else {
934 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "KStatusNotifierWatcher not reachable";
935 useLegacy = true;
936 }
937#else
938 useLegacy = true;
939#endif
940 setLegacySystemTrayEnabled(useLegacy);
941}
942
943void KStatusNotifierItemPrivate::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner)
944{
945 Q_UNUSED(name)
946 if (newOwner.isEmpty()) {
947 // unregistered
948 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Connection to the KStatusNotifierWatcher lost";
949 setLegacyMode(true);
950#if HAVE_DBUS
951 delete statusNotifierWatcher;
952 statusNotifierWatcher = nullptr;
953#endif
954 } else if (oldOwner.isEmpty()) {
955 // registered
956 setLegacyMode(false);
957 }
958}
959
960void KStatusNotifierItemPrivate::setLegacyMode(bool legacy)
961{
962 if (legacy) {
963 // unregistered
964 setLegacySystemTrayEnabled(true);
965 } else {
966 // registered
967 registerToDaemon();
968 }
969}
970
971void KStatusNotifierItemPrivate::legacyWheelEvent(int delta)
972{
973#if HAVE_DBUS
974 statusNotifierItemDBus->Scroll(delta, QStringLiteral("vertical"));
975#endif
976}
977
978void KStatusNotifierItemPrivate::legacyActivated(QSystemTrayIcon::ActivationReason reason)
979{
980 if (reason == QSystemTrayIcon::MiddleClick) {
981 Q_EMIT q->secondaryActivateRequested(systemTrayIcon->geometry().topLeft());
982 } else if (reason == QSystemTrayIcon::Trigger) {
983 q->activate(systemTrayIcon->geometry().topLeft());
984 }
985}
986
987void KStatusNotifierItemPrivate::setLegacySystemTrayEnabled(bool enabled)
988{
989 if (enabled == (systemTrayIcon != nullptr)) {
990 // already in the correct state
991 return;
992 }
993
994 if (enabled) {
995 bool isKde = !qEnvironmentVariableIsEmpty("KDE_FULL_SESSION")
996 || qgetenv("XDG_CURRENT_DESKTOP") == "KDE"
997 || qgetenv("QT_QPA_PLATFORMTHEME").toLower() == "kde";
998 if (!systemTrayIcon && !isKde) {
1000 return;
1001 }
1002 systemTrayIcon = new KStatusNotifierLegacyIcon(q);
1003 syncLegacySystemTrayIcon();
1004 systemTrayIcon->setToolTip(toolTipTitle);
1005 systemTrayIcon->show();
1006 QObject::connect(systemTrayIcon, SIGNAL(wheel(int)), q, SLOT(legacyWheelEvent(int)));
1007 QObject::connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), q, SLOT(legacyActivated(QSystemTrayIcon::ActivationReason)));
1008 } else if (isKde) {
1009 // prevent infinite recursion if the KDE platform plugin is loaded
1010 // but SNI is not available; see bug 350785
1011 qCWarning(LOG_KSTATUSNOTIFIERITEM) << "KDE platform plugin is loaded but SNI unavailable";
1012 return;
1013 }
1014
1015 if (menu) {
1016 menu->setWindowFlags(Qt::Popup);
1017 }
1018 } else {
1019 delete systemTrayIcon;
1020 systemTrayIcon = nullptr;
1021
1022 if (menu) {
1023 menu->setWindowFlags(Qt::Window);
1024 }
1025 }
1026
1027 if (menu) {
1028 QMenu *m = menu;
1029 menu = nullptr;
1030 q->setContextMenu(m);
1031 }
1032}
1033
1034void KStatusNotifierItemPrivate::syncLegacySystemTrayIcon()
1035{
1037#ifdef Q_OS_MACOS
1038 MacUtils::setBadgeLabelText(QString(QChar(0x26a0)) /*QStringLiteral("!")*/);
1039 if (attentionIconName.isNull() && attentionIcon.isNull()) {
1040 // code adapted from kmail's KMSystemTray::updateCount()
1041 int overlaySize = 22;
1042 QIcon attnIcon = qApp->windowIcon();
1043 if (!attnIcon.availableSizes().isEmpty()) {
1044 overlaySize = attnIcon.availableSizes().at(0).width();
1045 }
1047 labelFont.setBold(true);
1048 QFontMetrics qfm(labelFont);
1049 float attnHeight = overlaySize * 0.667;
1050 if (qfm.height() > attnHeight) {
1051 float labelSize = attnHeight;
1052 labelFont.setPointSizeF(labelSize);
1053 }
1054 // Paint the label in a pixmap
1055 QPixmap overlayPixmap(overlaySize, overlaySize);
1056 overlayPixmap.fill(Qt::transparent);
1057
1058 QPainter p(&overlayPixmap);
1059 p.setFont(labelFont);
1060 p.setBrush(Qt::NoBrush);
1061 // this sort of badge/label is red on OS X
1062 p.setPen(QColor(224, 0, 0));
1063 p.setOpacity(1.0);
1064 // use U+2022, the Unicode bullet
1065 p.drawText(overlayPixmap.rect(), Qt::AlignRight | Qt::AlignTop, QString(QChar(0x2022)));
1066 p.end();
1067
1068 QPixmap iconPixmap = attnIcon.pixmap(overlaySize, overlaySize);
1069 QPainter pp(&iconPixmap);
1070 pp.drawPixmap(0, 0, overlayPixmap);
1071 pp.end();
1072 systemTrayIcon->setIcon(iconPixmap);
1073 } else
1074#endif
1075 {
1076 if (!movieName.isNull()) {
1077 if (!movie) {
1078 movie = new QMovie(movieName);
1079 }
1080 systemTrayIcon->setMovie(movie);
1081 } else if (!attentionIconName.isNull()) {
1082 systemTrayIcon->setIcon(QIcon::fromTheme(attentionIconName));
1083 } else {
1084 systemTrayIcon->setIcon(attentionIcon);
1085 }
1086 }
1087 } else {
1088#ifdef Q_OS_MACOS
1089 if (!iconName.isNull()) {
1090 QIcon theIcon = QIcon::fromTheme(iconName);
1091 systemTrayIcon->setIconWithMask(theIcon, status == KStatusNotifierItem::Passive);
1092 } else {
1093 systemTrayIcon->setIconWithMask(icon, status == KStatusNotifierItem::Passive);
1094 }
1095 MacUtils::setBadgeLabelText(QString());
1096#else
1097 if (!iconName.isNull()) {
1098 systemTrayIcon->setIcon(QIcon::fromTheme(iconName));
1099 } else {
1100 systemTrayIcon->setIcon(icon);
1101 }
1102#endif
1103 }
1104
1105 systemTrayIcon->setToolTip(toolTipTitle);
1106}
1107
1108void KStatusNotifierItemPrivate::contextMenuAboutToShow()
1109{
1110 if (!hasQuit && standardActionsEnabled) {
1111 // we need to add the actions to the menu afterwards so that these items
1112 // appear at the _END_ of the menu
1113 menu->addSeparator();
1114 if (associatedWindow) {
1115 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore"));
1116
1117 if (action) {
1118 menu->addAction(action);
1119 }
1120 }
1121
1122 QAction *action = actionCollection.value(QStringLiteral("quit"));
1123
1124 if (action) {
1125 menu->addAction(action);
1126 }
1127
1128 hasQuit = true;
1129 }
1130
1131 if (associatedWindow) {
1132 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore"));
1133 if (checkVisibility(QPoint(0, 0), false)) {
1134 action->setText(KStatusNotifierItem::tr("&Restore", "@action:inmenu"));
1135 action->setIcon(QIcon::fromTheme(QStringLiteral("window-restore")));
1136 } else {
1137 action->setText(KStatusNotifierItem::tr("&Minimize", "@action:inmenu"));
1138 action->setIcon(QIcon::fromTheme(QStringLiteral("window-minimize")));
1139 }
1140 }
1141}
1142
1144{
1145 d->quitAborted = true;
1146}
1147
1148void KStatusNotifierItemPrivate::maybeQuit()
1149{
1150 Q_EMIT q->quitRequested();
1151
1152 if (quitAborted) {
1153 quitAborted = false;
1154 return;
1155 }
1156
1158 if (caption.isEmpty()) {
1160 }
1161
1162 const QString title = KStatusNotifierItem::tr("Confirm Quit From System Tray", "@title:window");
1163 const QString query = KStatusNotifierItem::tr("<qt>Are you sure you want to quit <b>%1</b>?</qt>").arg(caption);
1164
1165 auto *dialog = new QMessageBox(QMessageBox::Question, title, query, QMessageBox::NoButton);
1166 dialog->setAttribute(Qt::WA_DeleteOnClose);
1167
1168 auto *quitButton = dialog->addButton(KStatusNotifierItem::tr("Quit", "@action:button"), QMessageBox::AcceptRole);
1169 quitButton->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
1170 dialog->addButton(QMessageBox::Cancel);
1172 dialog->show();
1173 dialog->windowHandle()->setTransientParent(associatedWindow);
1174}
1175
1176void KStatusNotifierItemPrivate::minimizeRestore()
1177{
1178 q->activate(systemTrayIcon ? systemTrayIcon->geometry().topLeft() : QPoint(0, 0));
1179}
1180
1181void KStatusNotifierItemPrivate::hideMenu()
1182{
1183 menu->hide();
1184}
1185
1186void KStatusNotifierItemPrivate::minimizeRestore(bool show)
1187{
1188#if HAVE_X11
1190 KWindowInfo info(associatedWindow->winId(), NET::WMDesktop);
1191
1192 if (show) {
1193 if (onAllDesktops) {
1194 KX11Extras::setOnAllDesktops(associatedWindow->winId(), true);
1195 } else {
1196 KX11Extras::setCurrentDesktop(info.desktop());
1197 }
1198 } else {
1199 onAllDesktops = info.onAllDesktops();
1200 }
1201 }
1202#endif
1203
1204 if (show) {
1205 Qt::WindowState state = (Qt::WindowState)(associatedWindow->windowState() & ~Qt::WindowMinimized);
1206 associatedWindow->setWindowState(state);
1207 // Work around https://bugreports.qt.io/browse/QTBUG-120316
1208 if (auto *widgetwindow = static_cast<QWidgetWindow*>(associatedWindow->qt_metacast("QWidgetWindow"))) {
1209 widgetwindow->widget()->show();
1210 } else {
1211 associatedWindow->show();
1212 }
1213 associatedWindow->raise();
1214 KWindowSystem::activateWindow(associatedWindow);
1215 } else {
1216 // Work around https://bugreports.qt.io/browse/QTBUG-120316
1217 if (auto *widgetwindow = static_cast<QWidgetWindow*>(associatedWindow->qt_metacast("QWidgetWindow"))) {
1218 widgetwindow->widget()->hide();
1219 } else {
1220 associatedWindow->hide();
1221 }
1222 }
1223}
1224
1225#if HAVE_DBUS
1226KDbusImageStruct KStatusNotifierItemPrivate::imageToStruct(const QImage &image)
1227{
1228 KDbusImageStruct icon;
1229 icon.width = image.size().width();
1230 icon.height = image.size().height();
1231 if (image.format() == QImage::Format_ARGB32) {
1232 icon.data = QByteArray((char *)image.bits(), image.sizeInBytes());
1233 } else {
1235 icon.data = QByteArray((char *)image32.bits(), image32.sizeInBytes());
1236 }
1237
1238 // swap to network byte order if we are little endian
1240 quint32 *uintBuf = (quint32 *)icon.data.data();
1241 for (uint i = 0; i < icon.data.size() / sizeof(quint32); ++i) {
1242 *uintBuf = qToBigEndian(*uintBuf);
1243 ++uintBuf;
1244 }
1245 }
1246
1247 return icon;
1248}
1249
1250KDbusImageVector KStatusNotifierItemPrivate::iconToVector(const QIcon &icon)
1251{
1252 KDbusImageVector iconVector;
1253
1254 QPixmap iconPixmap;
1255
1256 // if an icon exactly that size wasn't found don't add it to the vector
1257 auto lstSizes = icon.availableSizes();
1258 if (lstSizes.isEmpty() && !icon.isNull()) {
1259 // if the icon is a svg icon, then available Sizes will be empty, try some common sizes
1260 lstSizes = {{16, 16}, {22, 22}, {32, 32}};
1261 }
1262 for (QSize size : lstSizes) {
1263 iconPixmap = icon.pixmap(size);
1264 if (!iconPixmap.isNull()) {
1265 iconVector.append(imageToStruct(iconPixmap.toImage()));
1266 }
1267 }
1268 return iconVector;
1269}
1270#endif
1271
1272#include "moc_kstatusnotifieritem.cpp"
1273#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 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 Mar 28 2025 11:59:28 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.