KNotifications

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

KDE's Doxygen guidelines are available online.