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
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") || qgetenv("XDG_CURRENT_DESKTOP") == "KDE";
996 if (!systemTrayIcon && !isKde) {
998 return;
999 }
1000 systemTrayIcon = new KStatusNotifierLegacyIcon(q);
1001 syncLegacySystemTrayIcon();
1002 systemTrayIcon->setToolTip(toolTipTitle);
1003 systemTrayIcon->show();
1004 QObject::connect(systemTrayIcon, SIGNAL(wheel(int)), q, SLOT(legacyWheelEvent(int)));
1005 QObject::connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), q, SLOT(legacyActivated(QSystemTrayIcon::ActivationReason)));
1006 } else if (isKde) {
1007 // prevent infinite recursion if the KDE platform plugin is loaded
1008 // but SNI is not available; see bug 350785
1009 qCWarning(LOG_KSTATUSNOTIFIERITEM) << "env says KDE is running but SNI unavailable -- check "
1010 "KDE_FULL_SESSION and XDG_CURRENT_DESKTOP";
1011 return;
1012 }
1013
1014 if (menu) {
1015 menu->setWindowFlags(Qt::Popup);
1016 }
1017 } else {
1018 delete systemTrayIcon;
1019 systemTrayIcon = nullptr;
1020
1021 if (menu) {
1022 menu->setWindowFlags(Qt::Window);
1023 }
1024 }
1025
1026 if (menu) {
1027 QMenu *m = menu;
1028 menu = nullptr;
1029 q->setContextMenu(m);
1030 }
1031}
1032
1033void KStatusNotifierItemPrivate::syncLegacySystemTrayIcon()
1034{
1036#ifdef Q_OS_MACOS
1037 MacUtils::setBadgeLabelText(QString(QChar(0x26a0)) /*QStringLiteral("!")*/);
1038 if (attentionIconName.isNull() && attentionIcon.isNull()) {
1039 // code adapted from kmail's KMSystemTray::updateCount()
1040 int overlaySize = 22;
1041 QIcon attnIcon = qApp->windowIcon();
1042 if (!attnIcon.availableSizes().isEmpty()) {
1043 overlaySize = attnIcon.availableSizes().at(0).width();
1044 }
1046 labelFont.setBold(true);
1047 QFontMetrics qfm(labelFont);
1048 float attnHeight = overlaySize * 0.667;
1049 if (qfm.height() > attnHeight) {
1050 float labelSize = attnHeight;
1051 labelFont.setPointSizeF(labelSize);
1052 }
1053 // Paint the label in a pixmap
1054 QPixmap overlayPixmap(overlaySize, overlaySize);
1055 overlayPixmap.fill(Qt::transparent);
1056
1057 QPainter p(&overlayPixmap);
1058 p.setFont(labelFont);
1059 p.setBrush(Qt::NoBrush);
1060 // this sort of badge/label is red on OS X
1061 p.setPen(QColor(224, 0, 0));
1062 p.setOpacity(1.0);
1063 // use U+2022, the Unicode bullet
1064 p.drawText(overlayPixmap.rect(), Qt::AlignRight | Qt::AlignTop, QString(QChar(0x2022)));
1065 p.end();
1066
1067 QPixmap iconPixmap = attnIcon.pixmap(overlaySize, overlaySize);
1068 QPainter pp(&iconPixmap);
1069 pp.drawPixmap(0, 0, overlayPixmap);
1070 pp.end();
1071 systemTrayIcon->setIcon(iconPixmap);
1072 } else
1073#endif
1074 {
1075 if (!movieName.isNull()) {
1076 if (!movie) {
1077 movie = new QMovie(movieName);
1078 }
1079 systemTrayIcon->setMovie(movie);
1080 } else if (!attentionIconName.isNull()) {
1081 systemTrayIcon->setIcon(QIcon::fromTheme(attentionIconName));
1082 } else {
1083 systemTrayIcon->setIcon(attentionIcon);
1084 }
1085 }
1086 } else {
1087#ifdef Q_OS_MACOS
1088 if (!iconName.isNull()) {
1089 QIcon theIcon = QIcon::fromTheme(iconName);
1090 systemTrayIcon->setIconWithMask(theIcon, status == KStatusNotifierItem::Passive);
1091 } else {
1092 systemTrayIcon->setIconWithMask(icon, status == KStatusNotifierItem::Passive);
1093 }
1094 MacUtils::setBadgeLabelText(QString());
1095#else
1096 if (!iconName.isNull()) {
1097 systemTrayIcon->setIcon(QIcon::fromTheme(iconName));
1098 } else {
1099 systemTrayIcon->setIcon(icon);
1100 }
1101#endif
1102 }
1103
1104 systemTrayIcon->setToolTip(toolTipTitle);
1105}
1106
1107void KStatusNotifierItemPrivate::contextMenuAboutToShow()
1108{
1109 if (!hasQuit && standardActionsEnabled) {
1110 // we need to add the actions to the menu afterwards so that these items
1111 // appear at the _END_ of the menu
1112 menu->addSeparator();
1113 if (associatedWindow) {
1114 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore"));
1115
1116 if (action) {
1117 menu->addAction(action);
1118 }
1119 }
1120
1121 QAction *action = actionCollection.value(QStringLiteral("quit"));
1122
1123 if (action) {
1124 menu->addAction(action);
1125 }
1126
1127 hasQuit = true;
1128 }
1129
1130 if (associatedWindow) {
1131 QAction *action = actionCollection.value(QStringLiteral("minimizeRestore"));
1132 if (checkVisibility(QPoint(0, 0), false)) {
1133 action->setText(KStatusNotifierItem::tr("&Restore", "@action:inmenu"));
1134 action->setIcon(QIcon::fromTheme(QStringLiteral("window-restore")));
1135 } else {
1136 action->setText(KStatusNotifierItem::tr("&Minimize", "@action:inmenu"));
1137 action->setIcon(QIcon::fromTheme(QStringLiteral("window-minimize")));
1138 }
1139 }
1140}
1141
1143{
1144 d->quitAborted = true;
1145}
1146
1147void KStatusNotifierItemPrivate::maybeQuit()
1148{
1149 Q_EMIT q->quitRequested();
1150
1151 if (quitAborted) {
1152 quitAborted = false;
1153 return;
1154 }
1155
1157 if (caption.isEmpty()) {
1159 }
1160
1161 const QString title = KStatusNotifierItem::tr("Confirm Quit From System Tray", "@title:window");
1162 const QString query = KStatusNotifierItem::tr("<qt>Are you sure you want to quit <b>%1</b>?</qt>").arg(caption);
1163
1164 auto *dialog = new QMessageBox(QMessageBox::Question, title, query, QMessageBox::NoButton);
1165 dialog->setAttribute(Qt::WA_DeleteOnClose);
1166
1167 auto *quitButton = dialog->addButton(KStatusNotifierItem::tr("Quit", "@action:button"), QMessageBox::AcceptRole);
1168 quitButton->setIcon(QIcon::fromTheme(QStringLiteral("application-exit")));
1169 dialog->addButton(QMessageBox::Cancel);
1171 dialog->show();
1172 dialog->windowHandle()->setTransientParent(associatedWindow);
1173}
1174
1175void KStatusNotifierItemPrivate::minimizeRestore()
1176{
1177 q->activate(systemTrayIcon ? systemTrayIcon->geometry().topLeft() : QPoint(0, 0));
1178}
1179
1180void KStatusNotifierItemPrivate::hideMenu()
1181{
1182 menu->hide();
1183}
1184
1185void KStatusNotifierItemPrivate::minimizeRestore(bool show)
1186{
1187#if HAVE_X11
1189 KWindowInfo info(associatedWindow->winId(), NET::WMDesktop);
1190
1191 if (show) {
1192 if (onAllDesktops) {
1193 KX11Extras::setOnAllDesktops(associatedWindow->winId(), true);
1194 } else {
1195 KX11Extras::setCurrentDesktop(info.desktop());
1196 }
1197 } else {
1198 onAllDesktops = info.onAllDesktops();
1199 }
1200 }
1201#endif
1202
1203 if (show) {
1204 Qt::WindowState state = (Qt::WindowState)(associatedWindow->windowState() & ~Qt::WindowMinimized);
1205 associatedWindow->setWindowState(state);
1206 // Work around https://bugreports.qt.io/browse/QTBUG-120316
1207 if (auto *widgetwindow = static_cast<QWidgetWindow*>(associatedWindow->qt_metacast("QWidgetWindow"))) {
1208 widgetwindow->widget()->show();
1209 } else {
1210 associatedWindow->show();
1211 }
1212 associatedWindow->raise();
1213 KWindowSystem::activateWindow(associatedWindow);
1214 } else {
1215 // Work around https://bugreports.qt.io/browse/QTBUG-120316
1216 if (auto *widgetwindow = static_cast<QWidgetWindow*>(associatedWindow->qt_metacast("QWidgetWindow"))) {
1217 widgetwindow->widget()->hide();
1218 } else {
1219 associatedWindow->hide();
1220 }
1221 }
1222}
1223
1224#if HAVE_DBUS
1225KDbusImageStruct KStatusNotifierItemPrivate::imageToStruct(const QImage &image)
1226{
1227 KDbusImageStruct icon;
1228 icon.width = image.size().width();
1229 icon.height = image.size().height();
1230 if (image.format() == QImage::Format_ARGB32) {
1231 icon.data = QByteArray((char *)image.bits(), image.sizeInBytes());
1232 } else {
1234 icon.data = QByteArray((char *)image32.bits(), image32.sizeInBytes());
1235 }
1236
1237 // swap to network byte order if we are little endian
1239 quint32 *uintBuf = (quint32 *)icon.data.data();
1240 for (uint i = 0; i < icon.data.size() / sizeof(quint32); ++i) {
1241 *uintBuf = qToBigEndian(*uintBuf);
1242 ++uintBuf;
1243 }
1244 }
1245
1246 return icon;
1247}
1248
1249KDbusImageVector KStatusNotifierItemPrivate::iconToVector(const QIcon &icon)
1250{
1251 KDbusImageVector iconVector;
1252
1253 QPixmap iconPixmap;
1254
1255 // if an icon exactly that size wasn't found don't add it to the vector
1256 const auto lstSizes = icon.availableSizes();
1257 for (QSize size : lstSizes) {
1258 iconPixmap = icon.pixmap(size);
1259 iconVector.append(imageToStruct(iconPixmap.toImage()));
1260 }
1261
1262 return iconVector;
1263}
1264#endif
1265
1266#include "moc_kstatusnotifieritem.cpp"
1267#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)
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 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 Mon Nov 18 2024 12:15:27 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.