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
60 : QObject(parent)
61 , d(new KStatusNotifierItemPrivate(this))
62{
63 d->init(QString());
64}
65
67 : QObject(parent)
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
548{
549 return d->actionCollection.values();
550}
551
553{
554 d->actionCollection.insert(name, action);
555}
556
558{
559 d->actionCollection.remove(name);
560}
561
563{
564 return d->actionCollection.value(name);
565}
566
568{
569 if (d->standardActionsEnabled == enabled) {
570 return;
571 }
572
573 d->standardActionsEnabled = enabled;
574
575 if (d->menu && !enabled && d->hasQuit) {
576 QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore"));
577 if (action) {
578 d->menu->removeAction(action);
579 }
580
581 action = d->actionCollection.value(QStringLiteral("quit"));
582 if (action) {
583 d->menu->removeAction(action);
584 }
585
586 d->hasQuit = false;
587 }
588}
589
591{
592 return d->standardActionsEnabled;
593}
594
595void KStatusNotifierItem::showMessage(const QString &title, const QString &message, const QString &icon, int timeout)
596{
597#if HAVE_DBUS
598 if (!d->notificationsClient) {
599 d->notificationsClient = new org::freedesktop::Notifications(QStringLiteral("org.freedesktop.Notifications"),
600 QStringLiteral("/org/freedesktop/Notifications"),
602 }
603
604 uint id = 0;
605 QVariantMap hints;
606
607 QString desktopFileName = QGuiApplication::desktopFileName();
608 if (!desktopFileName.isEmpty()) {
609 // handle apps which set the desktopFileName property with filename suffix,
610 // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521)
611 if (desktopFileName.endsWith(QLatin1String(".desktop"))) {
612 desktopFileName.chop(8);
613 }
614 hints.insert(QStringLiteral("desktop-entry"), desktopFileName);
615 }
616
617 d->notificationsClient->Notify(d->title, id, icon, title, message, QStringList(), hints, timeout);
618#else
619 if (d->systemTrayIcon) {
620 // Growl is not needed anymore for QSystemTrayIcon::showMessage() since OS X 10.8
621 d->systemTrayIcon->showMessage(title, message, QSystemTrayIcon::Information, timeout);
622 }
623#endif
624}
625
626QString KStatusNotifierItem::title() const
627{
628 return d->title;
629}
630
632{
633 // if the user activated the icon the NeedsAttention state is no longer necessary
634 // FIXME: always true?
635 if (d->status == NeedsAttention) {
636 d->status = Active;
637#ifdef Q_OS_MACOS
638 MacUtils::setBadgeLabelText(QString());
639#endif
640#if HAVE_DBUS
641 Q_EMIT d->statusNotifierItemDBus->NewStatus(
642 QString::fromLatin1(metaObject()->enumerator(metaObject()->indexOfEnumerator("ItemStatus")).valueToKey(d->status)));
643#endif
644 }
645
646 if (d->menu && d->menu->isVisible()) {
647 d->menu->hide();
648 }
649
650 if (!d->associatedWindow) {
651 Q_EMIT activateRequested(true, pos);
652 return;
653 }
654
655 d->checkVisibility(pos);
656}
657
659{
660 if (!d->associatedWindow) {
661 return;
662 }
663 d->minimizeRestore(false);
664}
665
667{
668#if HAVE_DBUS
669 return d->statusNotifierItemDBus->m_xdgActivationToken;
670#else
671 return {};
672#endif
673}
674
675bool KStatusNotifierItemPrivate::checkVisibility(QPoint pos, bool perform)
676{
677 // mapped = visible (but possibly obscured)
678 const bool mapped = associatedWindow->isVisible() && !(associatedWindow->windowState() & Qt::WindowMinimized);
679
680 // - not mapped -> show, raise, focus
681 // - mapped
682 // - obscured -> raise, focus
683 // - not obscured -> hide
684 // info1.mappingState() != NET::Visible -> window on another desktop?
685 if (!mapped) {
686 if (perform) {
687 minimizeRestore(true);
688 Q_EMIT q->activateRequested(true, pos);
689 }
690
691 return true;
692#if HAVE_X11
693 } else if (QGuiApplication::platformName() == QLatin1String("xcb")) {
695 const KWindowInfo info1(associatedWindow->winId(), NET::XAWMState | NET::WMState | NET::WMDesktop);
697 it.toBack();
698 while (it.hasPrevious()) {
699 WId id = it.previous();
700 if (id == associatedWindow->winId()) {
701 break;
702 }
703
704 KWindowInfo info2(id, NET::WMDesktop | NET::WMGeometry | NET::XAWMState | NET::WMState | NET::WMWindowType);
705
706 if (info2.mappingState() != NET::Visible) {
707 continue; // not visible on current desktop -> ignore
708 }
709
710 if (!info2.geometry().intersects(associatedWindow->geometry())) {
711 continue; // not obscuring the window -> ignore
712 }
713
714 if (!info1.hasState(NET::KeepAbove) && info2.hasState(NET::KeepAbove)) {
715 continue; // obscured by window kept above -> ignore
716 }
717
718 /* clang-format off */
719 static constexpr auto flags = (NET::NormalMask
729 /* clang-format on */
730 NET::WindowType type = info2.windowType(flags);
731
732 if (type == NET::Dock || type == NET::TopMenu) {
733 continue; // obscured by dock or topmenu -> ignore
734 }
735
736 if (perform) {
737 KX11Extras::forceActiveWindow(associatedWindow->winId());
738 Q_EMIT q->activateRequested(true, pos);
739 }
740
741 return true;
742 }
743
744 // not on current desktop?
745 if (!info1.isOnCurrentDesktop()) {
746 if (perform) {
747 KWindowSystem::activateWindow(associatedWindow);
748 Q_EMIT q->activateRequested(true, pos);
749 }
750
751 return true;
752 }
753
754 if (perform) {
755 minimizeRestore(false); // hide
756 Q_EMIT q->activateRequested(false, pos);
757 }
758 }
759 return false;
760#endif
761 } else {
762 if (perform) {
763 if (!associatedWindow->isActive()) {
764 KWindowSystem::activateWindow(associatedWindow);
765 Q_EMIT q->activateRequested(true, pos);
766 } else {
767 minimizeRestore(false); // hide
768 Q_EMIT q->activateRequested(false, pos);
769 }
770 }
771 return false;
772 }
773
774 return true;
775}
776
777bool KStatusNotifierItem::eventFilter(QObject *watched, QEvent *event)
778{
779 if (watched == d->associatedWindow) {
780 if (event->type() == QEvent::Show) {
781 d->associatedWindow->setPosition(d->associatedWindowPos);
782 } else if (event->type() == QEvent::Hide) {
783 d->associatedWindowPos = d->associatedWindow->position();
784 }
785 }
786
787 if (d->systemTrayIcon == nullptr) {
788 // FIXME: ugly ugly workaround to weird QMenu's focus problems
789 if (watched == d->menu
790 && (event->type() == QEvent::WindowDeactivate
791 || (event->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton))) {
792 // put at the back of even queue to let the action activate anyways
793 QTimer::singleShot(0, this, [this]() {
794 d->hideMenu();
795 });
796 }
797 }
798 return false;
799}
800
801// KStatusNotifierItemPrivate
802
803const int KStatusNotifierItemPrivate::s_protocolVersion = 0;
804
805KStatusNotifierItemPrivate::KStatusNotifierItemPrivate(KStatusNotifierItem *item)
806 : q(item)
807 , category(KStatusNotifierItem::ApplicationStatus)
808 , status(KStatusNotifierItem::Passive)
809 , movie(nullptr)
810 , systemTrayIcon(nullptr)
811 , menu(nullptr)
812 , associatedWindow(nullptr)
813 , titleAction(nullptr)
814 , hasQuit(false)
815 , onAllDesktops(false)
816 , standardActionsEnabled(true)
817{
818}
819
820void KStatusNotifierItemPrivate::init(const QString &extraId)
821{
822 QWidget *parentWidget = qobject_cast<QWidget *>(q->parent());
823
824 q->setAssociatedWindow(parentWidget ? parentWidget->window()->windowHandle() : nullptr);
825#if HAVE_DBUS
826 qDBusRegisterMetaType<KDbusImageStruct>();
827 qDBusRegisterMetaType<KDbusImageVector>();
828 qDBusRegisterMetaType<KDbusToolTipStruct>();
829
830 statusNotifierItemDBus = new KStatusNotifierItemDBus(q);
831
832 QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName),
835 q);
836 QObject::connect(watcher, SIGNAL(serviceOwnerChanged(QString, QString, QString)), q, SLOT(serviceChange(QString, QString, QString)));
837#endif
838
839 // create a default menu, just like in KSystemtrayIcon
840 QMenu *m = new QMenu(parentWidget);
841
843 if (title.isEmpty()) {
845 }
846#ifdef Q_OS_MACOS
847 // OS X doesn't have texted separators so we emulate QAction::addSection():
848 // we first add an action with the desired text (title) and icon
849 titleAction = m->addAction(qApp->windowIcon(), title);
850 // this action should be disabled
851 titleAction->setEnabled(false);
852 // Give the titleAction a visible menu icon:
853 // Systray icon and menu ("menu extra") are often used by applications that provide no other interface.
854 // It is thus reasonable to show the application icon in the menu; Finder, Dock and App Switcher
855 // all show it in addition to the application name (and Apple's input "menu extra" also shows icons).
856 titleAction->setIconVisibleInMenu(true);
857 m->addAction(titleAction);
858 // now add a regular separator
859 m->addSeparator();
860#else
861 titleAction = m->addSection(qApp->windowIcon(), title);
862 m->setTitle(title);
863#endif
864 q->setContextMenu(m);
865
866 QAction *action = new QAction(q);
867 action->setText(KStatusNotifierItem::tr("Quit", "@action:inmenu"));
868 action->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
869 // cannot yet convert to function-pointer-based connect:
870 // some apps like kalarm or korgac have a hack to rewire the connection
871 // of the "quit" action to a own slot, and rely on the name-based slot to disconnect
872 // TODO: extend KStatusNotifierItem API to support such needs
873 QObject::connect(action, SIGNAL(triggered()), q, SLOT(maybeQuit()));
874 actionCollection.insert(QStringLiteral("quit"), action);
875
876 id = title;
877 if (!extraId.isEmpty()) {
878 id.append(QLatin1Char('_')).append(extraId);
879 }
880
881 // Init iconThemePath to the app folder for now
883
884 registerToDaemon();
885}
886
887void KStatusNotifierItemPrivate::registerToDaemon()
888{
889 bool useLegacy = false;
890#if HAVE_DBUS
891 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Registering a client interface to the KStatusNotifierWatcher";
892 if (!statusNotifierWatcher) {
893 statusNotifierWatcher = new org::kde::StatusNotifierWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName),
894 QStringLiteral("/StatusNotifierWatcher"),
896 }
897
898 if (statusNotifierWatcher->isValid()) {
899 // get protocol version in async way
900 QDBusMessage msg = QDBusMessage::createMethodCall(QString::fromLatin1(s_statusNotifierWatcherServiceName),
901 QStringLiteral("/StatusNotifierWatcher"),
902 QStringLiteral("org.freedesktop.DBus.Properties"),
903 QStringLiteral("Get"));
904 msg.setArguments(QVariantList{QStringLiteral("org.kde.StatusNotifierWatcher"), QStringLiteral("ProtocolVersion")});
906 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, q);
907 QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, watcher] {
908 watcher->deleteLater();
909 QDBusPendingReply<QVariant> reply = *watcher;
910 if (reply.isError()) {
911 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Failed to read protocol version of KStatusNotifierWatcher";
912 setLegacySystemTrayEnabled(true);
913 } else {
914 bool ok = false;
915 const int protocolVersion = reply.value().toInt(&ok);
916 if (ok && protocolVersion == s_protocolVersion) {
917 statusNotifierWatcher->RegisterStatusNotifierItem(statusNotifierItemDBus->service());
918 setLegacySystemTrayEnabled(false);
919 } else {
920 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "KStatusNotifierWatcher has incorrect protocol version";
921 setLegacySystemTrayEnabled(true);
922 }
923 }
924 });
925 } else {
926 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "KStatusNotifierWatcher not reachable";
927 useLegacy = true;
928 }
929#else
930 useLegacy = true;
931#endif
932 setLegacySystemTrayEnabled(useLegacy);
933}
934
935void KStatusNotifierItemPrivate::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner)
936{
937 Q_UNUSED(name)
938 if (newOwner.isEmpty()) {
939 // unregistered
940 qCDebug(LOG_KSTATUSNOTIFIERITEM) << "Connection to the KStatusNotifierWatcher lost";
941 setLegacyMode(true);
942#if HAVE_DBUS
943 delete statusNotifierWatcher;
944 statusNotifierWatcher = nullptr;
945#endif
946 } else if (oldOwner.isEmpty()) {
947 // registered
948 setLegacyMode(false);
949 }
950}
951
952void KStatusNotifierItemPrivate::setLegacyMode(bool legacy)
953{
954 if (legacy) {
955 // unregistered
956 setLegacySystemTrayEnabled(true);
957 } else {
958 // registered
959 registerToDaemon();
960 }
961}
962
963void KStatusNotifierItemPrivate::legacyWheelEvent(int delta)
964{
965#if HAVE_DBUS
966 statusNotifierItemDBus->Scroll(delta, QStringLiteral("vertical"));
967#endif
968}
969
970void KStatusNotifierItemPrivate::legacyActivated(QSystemTrayIcon::ActivationReason reason)
971{
972 if (reason == QSystemTrayIcon::MiddleClick) {
973 Q_EMIT q->secondaryActivateRequested(systemTrayIcon->geometry().topLeft());
974 } else if (reason == QSystemTrayIcon::Trigger) {
975 q->activate(systemTrayIcon->geometry().topLeft());
976 }
977}
978
979void KStatusNotifierItemPrivate::setLegacySystemTrayEnabled(bool enabled)
980{
981 if (enabled == (systemTrayIcon != nullptr)) {
982 // already in the correct state
983 return;
984 }
985
986 if (enabled) {
987 bool isKde = !qEnvironmentVariableIsEmpty("KDE_FULL_SESSION") || qgetenv("XDG_CURRENT_DESKTOP") == "KDE";
988 if (!systemTrayIcon && !isKde) {
990 return;
991 }
992 systemTrayIcon = new KStatusNotifierLegacyIcon(q);
993 syncLegacySystemTrayIcon();
994 systemTrayIcon->setToolTip(toolTipTitle);
995 systemTrayIcon->show();
996 QObject::connect(systemTrayIcon, SIGNAL(wheel(int)), q, SLOT(legacyWheelEvent(int)));
997 QObject::connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), q, SLOT(legacyActivated(QSystemTrayIcon::ActivationReason)));
998 } else if (isKde) {
999 // prevent infinite recursion if the KDE platform plugin is loaded
1000 // but SNI is not available; see bug 350785
1001 qCWarning(LOG_KSTATUSNOTIFIERITEM) << "env says KDE is running but SNI unavailable -- check "
1002 "KDE_FULL_SESSION and XDG_CURRENT_DESKTOP";
1003 return;
1004 }
1005
1006 if (menu) {
1007 menu->setWindowFlags(Qt::Popup);
1008 }
1009 } else {
1010 delete systemTrayIcon;
1011 systemTrayIcon = nullptr;
1012
1013 if (menu) {
1014 menu->setWindowFlags(Qt::Window);
1015 }
1016 }
1017
1018 if (menu) {
1019 QMenu *m = menu;
1020 menu = nullptr;
1021 q->setContextMenu(m);
1022 }
1023}
1024
1025void KStatusNotifierItemPrivate::syncLegacySystemTrayIcon()
1026{
1028#ifdef Q_OS_MACOS
1029 MacUtils::setBadgeLabelText(QString(QChar(0x26a0)) /*QStringLiteral("!")*/);
1030 if (attentionIconName.isNull() && attentionIcon.isNull()) {
1031 // code adapted from kmail's KMSystemTray::updateCount()
1032 int overlaySize = 22;
1033 QIcon attnIcon = qApp->windowIcon();
1034 if (!attnIcon.availableSizes().isEmpty()) {
1035 overlaySize = attnIcon.availableSizes().at(0).width();
1036 }
1038 labelFont.setBold(true);
1039 QFontMetrics qfm(labelFont);
1040 float attnHeight = overlaySize * 0.667;
1041 if (qfm.height() > attnHeight) {
1042 float labelSize = attnHeight;
1043 labelFont.setPointSizeF(labelSize);
1044 }
1045 // Paint the label in a pixmap
1046 QPixmap overlayPixmap(overlaySize, overlaySize);
1047 overlayPixmap.fill(Qt::transparent);
1048
1049 QPainter p(&overlayPixmap);
1050 p.setFont(labelFont);
1051 p.setBrush(Qt::NoBrush);
1052 // this sort of badge/label is red on OS X
1053 p.setPen(QColor(224, 0, 0));
1054 p.setOpacity(1.0);
1055 // use U+2022, the Unicode bullet
1056 p.drawText(overlayPixmap.rect(), Qt::AlignRight | Qt::AlignTop, QString(QChar(0x2022)));
1057 p.end();
1058
1059 QPixmap iconPixmap = attnIcon.pixmap(overlaySize, overlaySize);
1060 QPainter pp(&iconPixmap);
1061 pp.drawPixmap(0, 0, overlayPixmap);
1062 pp.end();
1063 systemTrayIcon->setIcon(iconPixmap);
1064 } else
1065#endif
1066 {
1067 if (!movieName.isNull()) {
1068 if (!movie) {
1069 movie = new QMovie(movieName);
1070 }
1071 systemTrayIcon->setMovie(movie);
1072 } else if (!attentionIconName.isNull()) {
1073 systemTrayIcon->setIcon(QIcon::fromTheme(attentionIconName));
1074 } else {
1075 systemTrayIcon->setIcon(attentionIcon);
1076 }
1077 }
1078 } else {
1079#ifdef Q_OS_MACOS
1080 if (!iconName.isNull()) {
1081 QIcon theIcon = QIcon::fromTheme(iconName);
1082 systemTrayIcon->setIconWithMask(theIcon, status == KStatusNotifierItem::Passive);
1083 } else {
1084 systemTrayIcon->setIconWithMask(icon, status == KStatusNotifierItem::Passive);
1085 }
1086 MacUtils::setBadgeLabelText(QString());
1087#else
1088 if (!iconName.isNull()) {
1089 systemTrayIcon->setIcon(QIcon::fromTheme(iconName));
1090 } else {
1091 systemTrayIcon->setIcon(icon);
1092 }
1093#endif
1094 }
1095
1096 systemTrayIcon->setToolTip(toolTipTitle);
1097}
1098
1099void KStatusNotifierItemPrivate::contextMenuAboutToShow()
1100{
1101 if (!hasQuit && standardActionsEnabled) {
1102 // we need to add the actions to the menu afterwards so that these items
1103 // appear at the _END_ of the menu
1104 menu->addSeparator();
1105 if (associatedWindow) {
1106 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore"));
1107
1108 if (action) {
1109 menu->addAction(action);
1110 }
1111 }
1112
1113 QAction *action = actionCollection.value(QStringLiteral("quit"));
1114
1115 if (action) {
1116 menu->addAction(action);
1117 }
1118
1119 hasQuit = true;
1120 }
1121
1122 if (associatedWindow) {
1123 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore"));
1124 if (checkVisibility(QPoint(0, 0), false)) {
1125 action->setText(KStatusNotifierItem::tr("&Restore", "@action:inmenu"));
1126 action->setIcon(QIcon::fromTheme(QStringLiteral("window-restore")));
1127 } else {
1128 action->setText(KStatusNotifierItem::tr("&Minimize", "@action:inmenu"));
1129 action->setIcon(QIcon::fromTheme(QStringLiteral("window-minimize")));
1130 }
1131 }
1132}
1133
1134void KStatusNotifierItemPrivate::maybeQuit()
1135{
1137 if (caption.isEmpty()) {
1139 }
1140
1141 const QString title = KStatusNotifierItem::tr("Confirm Quit From System Tray", "@title:window");
1142 const QString query = KStatusNotifierItem::tr("<qt>Are you sure you want to quit <b>%1</b>?</qt>").arg(caption);
1143
1144 auto *dialog = new QMessageBox(QMessageBox::Question, title, query, QMessageBox::NoButton);
1145 dialog->setAttribute(Qt::WA_DeleteOnClose);
1146
1147 auto *quitButton = dialog->addButton(KStatusNotifierItem::tr("Quit", "@action:button"), QMessageBox::AcceptRole);
1148 quitButton->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
1149 dialog->addButton(QMessageBox::Cancel);
1151 dialog->show();
1152 dialog->windowHandle()->setTransientParent(associatedWindow);
1153}
1154
1155void KStatusNotifierItemPrivate::minimizeRestore()
1156{
1157 q->activate(systemTrayIcon ? systemTrayIcon->geometry().topLeft() : QPoint(0, 0));
1158}
1159
1160void KStatusNotifierItemPrivate::hideMenu()
1161{
1162 menu->hide();
1163}
1164
1165void KStatusNotifierItemPrivate::minimizeRestore(bool show)
1166{
1167#if HAVE_X11
1169 KWindowInfo info(associatedWindow->winId(), NET::WMDesktop);
1170
1171 if (show) {
1172 if (onAllDesktops) {
1173 KX11Extras::setOnAllDesktops(associatedWindow->winId(), true);
1174 } else {
1175 KX11Extras::setCurrentDesktop(info.desktop());
1176 }
1177 } else {
1178 onAllDesktops = info.onAllDesktops();
1179 }
1180 }
1181#endif
1182
1183 if (show) {
1184 Qt::WindowState state = (Qt::WindowState)(associatedWindow->windowState() & ~Qt::WindowMinimized);
1185 associatedWindow->setWindowState(state);
1186 // Work around https://bugreports.qt.io/browse/QTBUG-120316
1187 if (auto *widgetwindow = static_cast<QWidgetWindow*>(associatedWindow->qt_metacast("QWidgetWindow"))) {
1188 widgetwindow->widget()->show();
1189 } else {
1190 associatedWindow->show();
1191 }
1192 associatedWindow->raise();
1193 KWindowSystem::activateWindow(associatedWindow);
1194 } else {
1195 // Work around https://bugreports.qt.io/browse/QTBUG-120316
1196 if (auto *widgetwindow = static_cast<QWidgetWindow*>(associatedWindow->qt_metacast("QWidgetWindow"))) {
1197 widgetwindow->widget()->hide();
1198 } else {
1199 associatedWindow->hide();
1200 }
1201 }
1202}
1203
1204#if HAVE_DBUS
1205KDbusImageStruct KStatusNotifierItemPrivate::imageToStruct(const QImage &image)
1206{
1207 KDbusImageStruct icon;
1208 icon.width = image.size().width();
1209 icon.height = image.size().height();
1210 if (image.format() == QImage::Format_ARGB32) {
1211 icon.data = QByteArray((char *)image.bits(), image.sizeInBytes());
1212 } else {
1214 icon.data = QByteArray((char *)image32.bits(), image32.sizeInBytes());
1215 }
1216
1217 // swap to network byte order if we are little endian
1219 quint32 *uintBuf = (quint32 *)icon.data.data();
1220 for (uint i = 0; i < icon.data.size() / sizeof(quint32); ++i) {
1221 *uintBuf = qToBigEndian(*uintBuf);
1222 ++uintBuf;
1223 }
1224 }
1225
1226 return icon;
1227}
1228
1229KDbusImageVector KStatusNotifierItemPrivate::iconToVector(const QIcon &icon)
1230{
1231 KDbusImageVector iconVector;
1232
1233 QPixmap iconPixmap;
1234
1235 // if an icon exactly that size wasn't found don't add it to the vector
1236 const auto lstSizes = icon.availableSizes();
1237 for (QSize size : lstSizes) {
1238 iconPixmap = icon.pixmap(size);
1239 iconVector.append(imageToStruct(iconPixmap.toImage()));
1240 }
1241
1242 return iconVector;
1243}
1244#endif
1245
1246#include "moc_kstatusnotifieritem.cpp"
1247#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 addAction(const QString &name, QAction *action)
Adds an action to the actionCollection()
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...
QAction * action(const QString &name) const
Retrieves an action from the action collection by the action name.
void removeAction(const QString &name)
Removes an action from the collection.
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.
QList< QAction * > actionCollection() const
All the actions present in the menu.
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()
KSERVICE_EXPORT KService::List query(FilterFunc filterFunc)
VehicleSection::Type type(QStringView coachNumber, QStringView coachClassification)
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
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)
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)
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
QString tr(const char *sourceText, const char *disambiguation, int n)
void drawPixmap(const QPoint &point, const QPixmap &pixmap)
bool end()
int height() const const
QImage toImage() const const
int width() const const
Qt::MouseButton button() const const
int height() const const
int width() const const
QString locate(StandardLocation type, const QString &fileName, LocateOptions options)
QString & append(QChar ch)
QString arg(Args &&... args) const const
void chop(qsizetype n)
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-2024 The KDE developers.
Generated on Fri Jul 26 2024 11:51:11 by doxygen 1.11.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.