Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qdbustrayicon.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
6
7#ifndef QT_NO_SYSTEMTRAYICON
8
9#include <QString>
10#include <QDebug>
11#include <QRect>
12#include <QLoggingCategory>
13#include <QStandardPaths>
14#include <QFileInfo>
15#include <QDir>
16#include <QMetaObject>
17#include <QMetaEnum>
18#include <QDBusConnectionInterface>
19#include <QDBusArgument>
20#include <QDBusMetaType>
21#include <QDBusServiceWatcher>
22
23#include <qpa/qplatformmenu.h>
24#include <qpa/qplatformintegration.h>
25#include <qpa/qplatformservices.h>
26
27#include <private/qdbusmenuconnection_p.h>
28#include <private/qstatusnotifieritemadaptor_p.h>
29#include <private/qdbusmenuadaptor_p.h>
30#include <private/qdbusplatformmenu_p.h>
31#include <private/qxdgnotificationproxy_p.h>
32#include <private/qlockfile_p.h>
33#include <private/qguiapplication_p.h>
34
35// Defined in Windows headers which get included by qlockfile_p.h
36#undef interface
37
38QT_BEGIN_NAMESPACE
39
40using namespace Qt::StringLiterals;
41
42Q_LOGGING_CATEGORY(qLcTray, "qt.qpa.tray")
43
45{
46 QString tempPath = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation);
47 if (!tempPath.isEmpty()) {
48 QString flatpakId = qEnvironmentVariable("FLATPAK_ID");
49 if (!flatpakId.isEmpty() && QFileInfo::exists("/.flatpak-info"_L1))
50 tempPath += "/app/"_L1 + flatpakId;
51 return tempPath;
52 }
53
54 tempPath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
55
56 if (!tempPath.isEmpty()) {
57 QDir tempDir(tempPath);
58 if (tempDir.exists())
59 return tempPath;
60
61 if (tempDir.mkpath(QStringLiteral("."))) {
62 const QFile::Permissions permissions = QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
63 if (QFile(tempPath).setPermissions(permissions))
64 return tempPath;
65 }
66 }
67
68 return QDir::tempPath();
69}
70
71static const QString KDEItemFormat = QStringLiteral("org.kde.StatusNotifierItem-%1-%2");
72static const QString KDEWatcherService = QStringLiteral("org.kde.StatusNotifierWatcher");
73static const QString XdgNotificationService = QStringLiteral("org.freedesktop.Notifications");
74static const QString XdgNotificationPath = QStringLiteral("/org/freedesktop/Notifications");
75static const QString DefaultAction = QStringLiteral("default");
76static int instanceCount = 0;
77
79{
80 static const QString TempFileTemplate = iconTempPath() + "/qt-trayicon-XXXXXX.png"_L1;
81 return TempFileTemplate;
82}
83
84QDBusArgument &operator<<(QDBusArgument &argument, const QDBusTrayImage &img)
85{
86 QImage image = img.ico.pixmap(QSize(64, 64), img.dpr).toImage();
87 const bool hasAlpha = image.hasAlphaChannel();
88 image = image.convertToFormat(hasAlpha ? QImage::Format_RGBA8888 : QImage::Format_RGB888);
89
90 argument.beginStructure();
91 argument << image.width() << image.height()
92 << image.bytesPerLine() << hasAlpha
93 << 8 << (hasAlpha ? 4 : 3)
94 << QByteArray((const char *)image.constBits(), image.sizeInBytes());
95 argument.endStructure();
96 return argument;
97}
98const QDBusArgument &operator>>(const QDBusArgument &argument, QDBusTrayImage &)
99{
100 Q_ASSERT_X(false, __FUNCTION__, "Not implemented");
101 return argument;
102}
103
104/*!
105 \class QDBusTrayIcon
106 \internal
107*/
108
109QDBusTrayIcon::QDBusTrayIcon()
110 : m_dbusConnection(nullptr)
111 , m_adaptor(new QStatusNotifierItemAdaptor(this))
112 , m_menuAdaptor(nullptr)
113 , m_menu(nullptr)
114 , m_notifier(nullptr)
115 , m_instanceId(KDEItemFormat.arg(QCoreApplication::applicationPid()).arg(++instanceCount))
116 , m_category(QStringLiteral("ApplicationStatus"))
117 , m_defaultStatus(QStringLiteral("Active")) // be visible all the time. QSystemTrayIcon has no API to control this.
118 , m_status(m_defaultStatus)
119 , m_tempIcon(nullptr)
120 , m_tempAttentionIcon(nullptr)
121 , m_registered(false)
122{
123 qCDebug(qLcTray);
124 if (instanceCount == 1) {
125 QDBusMenuItem::registerDBusTypes();
126 qDBusRegisterMetaType<QXdgDBusImageStruct>();
127 qDBusRegisterMetaType<QXdgDBusImageVector>();
128 qDBusRegisterMetaType<QXdgDBusToolTipStruct>();
129 }
130 connect(this, SIGNAL(statusChanged(QString)), m_adaptor, SIGNAL(NewStatus(QString)));
131 connect(this, SIGNAL(tooltipChanged()), m_adaptor, SIGNAL(NewToolTip()));
132 connect(this, SIGNAL(iconChanged()), m_adaptor, SIGNAL(NewIcon()));
133 connect(this, SIGNAL(attention()), m_adaptor, SIGNAL(NewAttentionIcon()));
134 connect(this, SIGNAL(menuChanged()), m_adaptor, SIGNAL(NewMenu()));
135 connect(this, SIGNAL(attention()), m_adaptor, SIGNAL(NewTitle()));
136 connect(&m_attentionTimer, SIGNAL(timeout()), this, SLOT(attentionTimerExpired()));
137 m_attentionTimer.setSingleShot(true);
138}
139
143
145{
146 qCDebug(qLcTray) << "registering" << m_instanceId;
147 m_registered = dBusConnection()->registerTrayIcon(this);
148 QObject::connect(dBusConnection()->dbusWatcher(), &QDBusServiceWatcher::serviceRegistered,
149 this, &QDBusTrayIcon::watcherServiceRegistered);
150}
151
153{
154 qCDebug(qLcTray) << "unregistering" << m_instanceId;
155 if (m_registered)
156 dBusConnection()->unregisterTrayIcon(this);
157 delete m_dbusConnection;
158 m_dbusConnection = nullptr;
159 delete m_notifier;
160 m_notifier = nullptr;
161 m_registered = false;
162}
163
164void QDBusTrayIcon::watcherServiceRegistered(const QString &serviceName)
165{
166 Q_UNUSED(serviceName);
167 // We have the icon registered, but the watcher has restarted or
168 // changed, so we need to tell it about our icon again
169 if (m_registered)
170 dBusConnection()->registerTrayIconWithWatcher(this);
171}
172
173void QDBusTrayIcon::attentionTimerExpired()
174{
175 m_messageTitle = QString();
176 m_message = QString();
177 m_attentionIcon = QIcon();
178 emit attention();
179 emit tooltipChanged();
180 setStatus(m_defaultStatus);
181}
182
183void QDBusTrayIcon::setStatus(const QString &status)
184{
185 qCDebug(qLcTray) << status;
186 if (m_status == status)
187 return;
188 m_status = status;
189 emit statusChanged(m_status);
190}
191
192QTemporaryFile *QDBusTrayIcon::tempIcon(const QIcon &icon)
193{
194 // Hack for indicator-application, which doesn't handle icons sent across D-Bus:
195 // save the icon to a temp file and set the icon name to that filename.
196 static bool necessity_checked = false;
197 static bool necessary = false;
198 if (!necessity_checked) {
199 QDBusConnection session = QDBusConnection::sessionBus();
200 uint pid = session.interface()->servicePid(KDEWatcherService).value();
201 QString processName = QLockFilePrivate::processNameByPid(pid);
202 necessary = processName.endsWith("indicator-application-service"_L1);
203 if (!necessary) {
204 necessary = session.interface()->isServiceRegistered(
205 QStringLiteral("com.canonical.indicator.application"));
206 }
207 if (!necessary) {
208 necessary = session.interface()->isServiceRegistered(
209 QStringLiteral("org.ayatana.indicator.application"));
210 }
211 if (!necessary && QGuiApplication::desktopSettingsAware()) {
212 // Accessing to process name might be not allowed if the application
213 // is confined, thus we can just rely on the current desktop in use
214 const QPlatformServices *services = QGuiApplicationPrivate::platformIntegration()->services();
215 if (services)
216 necessary = services->desktopEnvironment().split(':').contains("UNITY");
217 }
218 necessity_checked = true;
219 }
220 if (!necessary)
221 return nullptr;
222 QTemporaryFile *ret = new QTemporaryFile(tempFileTemplate(), this);
223 if (!ret->open()) {
224 delete ret;
225 return nullptr;
226 }
227 icon.pixmap(QSize(22, 22)).save(ret);
228 ret->close();
229 return ret;
230}
231
233{
234 if (!m_dbusConnection) {
235 m_dbusConnection = new QDBusMenuConnection(this, m_instanceId);
236 m_notifier = new QXdgNotificationInterface(XdgNotificationService,
237 XdgNotificationPath, m_dbusConnection->connection(), this);
238 connect(m_notifier, SIGNAL(NotificationClosed(uint,uint)), this, SLOT(notificationClosed(uint,uint)));
239 connect(m_notifier, SIGNAL(ActionInvoked(uint,QString)), this, SLOT(actionInvoked(uint,QString)));
240 }
241 return m_dbusConnection;
242}
243
244void QDBusTrayIcon::updateIcon(const QIcon &icon)
245{
246 m_iconName = icon.name();
247 m_icon = icon;
248 if (m_iconName.isEmpty()) {
249 if (m_tempIcon)
250 delete m_tempIcon;
251 m_tempIcon = tempIcon(icon);
252 if (m_tempIcon)
253 m_iconName = m_tempIcon->fileName();
254 }
255 qCDebug(qLcTray) << m_iconName << icon.availableSizes();
256 emit iconChanged();
257}
258
259void QDBusTrayIcon::updateToolTip(const QString &tooltip)
260{
261 qCDebug(qLcTray) << tooltip;
262 m_tooltip = tooltip;
263 emit tooltipChanged();
264}
265
267{
268 return new QDBusPlatformMenu();
269}
270
271void QDBusTrayIcon::updateMenu(QPlatformMenu * menu)
272{
273 qCDebug(qLcTray) << menu;
274 QDBusPlatformMenu *newMenu = qobject_cast<QDBusPlatformMenu *>(menu);
275 if (m_menu != newMenu) {
276 if (m_menu) {
277 dBusConnection()->unregisterTrayIconMenu(this);
278 delete m_menuAdaptor;
279 }
280 m_menu = newMenu;
281 m_menuAdaptor = new QDBusMenuAdaptor(m_menu);
282 // TODO connect(m_menu, , m_menuAdaptor, SIGNAL(ItemActivationRequested(int,uint)));
283 connect(m_menu, SIGNAL(propertiesUpdated(QDBusMenuItemList,QDBusMenuItemKeysList)),
284 m_menuAdaptor, SIGNAL(ItemsPropertiesUpdated(QDBusMenuItemList,QDBusMenuItemKeysList)));
285 connect(m_menu, SIGNAL(updated(uint,int)),
286 m_menuAdaptor, SIGNAL(LayoutUpdated(uint,int)));
287 dBusConnection()->registerTrayIconMenu(this);
288 emit menuChanged();
289 }
290}
291
292void QDBusTrayIcon::showMessage(const QString &title, const QString &msg, const QIcon &icon,
293 QPlatformSystemTrayIcon::MessageIcon iconType, int msecs)
294{
295 m_messageTitle = title;
296 m_message = msg;
297 m_attentionIcon = icon;
298 QStringList notificationActions;
299 switch (iconType) {
300 case Information:
301 m_attentionIconName = QStringLiteral("dialog-information");
302 break;
303 case Warning:
304 m_attentionIconName = QStringLiteral("dialog-warning");
305 break;
306 case Critical:
307 m_attentionIconName = QStringLiteral("dialog-error");
308 // If there are actions, the desktop notification may appear as a message dialog
309 // with button(s), which will interrupt the user and require a response.
310 // That is an optional feature in implementations of org.freedesktop.Notifications
311 notificationActions << DefaultAction << tr("OK");
312 break;
313 default:
314 m_attentionIconName.clear();
315 break;
316 }
317 if (m_attentionIconName.isEmpty()) {
318 if (m_tempAttentionIcon)
319 delete m_tempAttentionIcon;
320 m_tempAttentionIcon = tempIcon(icon);
321 if (m_tempAttentionIcon)
322 m_attentionIconName = m_tempAttentionIcon->fileName();
323 }
324 qCDebug(qLcTray) << title << msg <<
325 QPlatformSystemTrayIcon::metaObject()->enumerator(
326 QPlatformSystemTrayIcon::staticMetaObject.indexOfEnumerator("MessageIcon")).valueToKey(iconType)
327 << m_attentionIconName << msecs;
328 setStatus(QStringLiteral("NeedsAttention"));
329 m_attentionTimer.start(msecs);
330 emit tooltipChanged();
331 emit attention();
332
333 // Desktop notification
334 QVariantMap hints;
335 // urgency levels according to https://developer.gnome.org/notification-spec/#urgency-levels
336 // 0 low, 1 normal, 2 critical
337 int urgency = static_cast<int>(iconType) - 1;
338 if (urgency < 0) // no icon
339 urgency = 0;
340 hints.insert("urgency"_L1, QVariant(urgency));
341 if (m_attentionIconName.isEmpty()) {
342 qDBusRegisterMetaType<QDBusTrayImage>();
343 const QDBusTrayImage img = {icon, qGuiApp->devicePixelRatio()};
344 hints.insert("image_data"_L1, QVariant::fromValue(img));
345 }
346 m_notifier->notify(QCoreApplication::applicationName(), 0,
347 m_attentionIconName, title, msg, notificationActions, hints, msecs);
348}
349
350void QDBusTrayIcon::actionInvoked(uint id, const QString &action)
351{
352 qCDebug(qLcTray) << id << action;
353 emit messageClicked();
354}
355
356void QDBusTrayIcon::notificationClosed(uint id, uint reason)
357{
358 qCDebug(qLcTray) << id << reason;
359}
360
362{
363 QDBusMenuConnection * conn = const_cast<QDBusTrayIcon *>(this)->dBusConnection();
364
365 // If the KDE watcher service is registered, we must be on a desktop
366 // where a StatusNotifier-conforming system tray exists.
367 qCDebug(qLcTray) << conn->isWatcherRegistered();
368 return conn->isWatcherRegistered();
369}
370
371QT_END_NAMESPACE
372
373#include "moc_qdbustrayicon_p.cpp"
374#endif //QT_NO_SYSTEMTRAYICON
void init() override
This method is called to initialize the platform dependent implementation.
void updateMenu(QPlatformMenu *menu) override
This method is called when the system tray menu did change.
void menuChanged()
void showMessage(const QString &title, const QString &msg, const QIcon &icon, MessageIcon iconType, int msecs) override
Shows a balloon message for the entry with the given title, message msg and icon for the time specifi...
QDBusMenuConnection * dBusConnection()
void cleanup() override
This method is called to cleanup the platform dependent implementation.
virtual ~QDBusTrayIcon()
void attention()
void tooltipChanged()
void updateIcon(const QIcon &icon) override
This method is called when the icon did change.
void iconChanged()
void updateToolTip(const QString &tooltip) override
This method is called when the tooltip text did change.
bool isSystemTrayAvailable() const override
Returns true if the system tray is available on the platform.
QPlatformMenu * createMenu() const override
This method allows platforms to use a different QPlatformMenu for system tray menus than what would n...
QDBusArgument & operator<<(QDBusArgument &argument, const QDBusTrayImage &img)
static const QString KDEWatcherService
static int instanceCount
static const QString DefaultAction
static const QString KDEItemFormat
static QString tempFileTemplate()
const QDBusArgument & operator>>(const QDBusArgument &argument, QDBusTrayImage &)
static const QString XdgNotificationPath
static QString iconTempPath()
static const QString XdgNotificationService
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")