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
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
84/*!
85 \class QDBusTrayIcon
86 \internal
87*/
88
89QDBusTrayIcon::QDBusTrayIcon()
90 : m_dbusConnection(nullptr)
91 , m_adaptor(new QStatusNotifierItemAdaptor(this))
92 , m_menuAdaptor(nullptr)
93 , m_menu(nullptr)
94 , m_notifier(nullptr)
95 , m_instanceId(KDEItemFormat.arg(QCoreApplication::applicationPid()).arg(++instanceCount))
96 , m_category(QStringLiteral("ApplicationStatus"))
97 , m_defaultStatus(QStringLiteral("Active")) // be visible all the time. QSystemTrayIcon has no API to control this.
98 , m_status(m_defaultStatus)
99 , m_tempIcon(nullptr)
100 , m_tempAttentionIcon(nullptr)
101 , m_registered(false)
102{
103 qCDebug(qLcTray);
104 if (instanceCount == 1) {
105 QDBusMenuItem::registerDBusTypes();
106 qDBusRegisterMetaType<QXdgDBusImageStruct>();
107 qDBusRegisterMetaType<QXdgDBusImageVector>();
108 qDBusRegisterMetaType<QXdgDBusToolTipStruct>();
109 }
110 connect(this, SIGNAL(statusChanged(QString)), m_adaptor, SIGNAL(NewStatus(QString)));
111 connect(this, SIGNAL(tooltipChanged()), m_adaptor, SIGNAL(NewToolTip()));
112 connect(this, SIGNAL(iconChanged()), m_adaptor, SIGNAL(NewIcon()));
113 connect(this, SIGNAL(attention()), m_adaptor, SIGNAL(NewAttentionIcon()));
114 connect(this, SIGNAL(menuChanged()), m_adaptor, SIGNAL(NewMenu()));
115 connect(this, SIGNAL(attention()), m_adaptor, SIGNAL(NewTitle()));
116 connect(&m_attentionTimer, SIGNAL(timeout()), this, SLOT(attentionTimerExpired()));
117 m_attentionTimer.setSingleShot(true);
118}
119
123
125{
126 qCDebug(qLcTray) << "registering" << m_instanceId;
127 m_registered = dBusConnection()->registerTrayIcon(this);
128 QObject::connect(dBusConnection()->dbusWatcher(), &QDBusServiceWatcher::serviceRegistered,
129 this, &QDBusTrayIcon::watcherServiceRegistered);
130}
131
133{
134 qCDebug(qLcTray) << "unregistering" << m_instanceId;
135 if (m_registered)
136 dBusConnection()->unregisterTrayIcon(this);
137 delete m_dbusConnection;
138 m_dbusConnection = nullptr;
139 delete m_notifier;
140 m_notifier = nullptr;
141 m_registered = false;
142}
143
144void QDBusTrayIcon::watcherServiceRegistered(const QString &serviceName)
145{
146 Q_UNUSED(serviceName);
147 // We have the icon registered, but the watcher has restarted or
148 // changed, so we need to tell it about our icon again
149 if (m_registered)
150 dBusConnection()->registerTrayIconWithWatcher(this);
151}
152
153void QDBusTrayIcon::attentionTimerExpired()
154{
155 m_messageTitle = QString();
156 m_message = QString();
157 m_attentionIcon = QIcon();
158 emit attention();
159 emit tooltipChanged();
160 setStatus(m_defaultStatus);
161}
162
163void QDBusTrayIcon::setStatus(const QString &status)
164{
165 qCDebug(qLcTray) << status;
166 if (m_status == status)
167 return;
168 m_status = status;
169 emit statusChanged(m_status);
170}
171
172QTemporaryFile *QDBusTrayIcon::tempIcon(const QIcon &icon)
173{
174 // Hack for indicator-application, which doesn't handle icons sent across D-Bus:
175 // save the icon to a temp file and set the icon name to that filename.
176 static bool necessity_checked = false;
177 static bool necessary = false;
178 if (!necessity_checked) {
179 QDBusConnection session = QDBusConnection::sessionBus();
180 uint pid = session.interface()->servicePid(KDEWatcherService).value();
181 QString processName = QLockFilePrivate::processNameByPid(pid);
182 necessary = processName.endsWith("indicator-application-service"_L1);
183 if (!necessary) {
184 necessary = session.interface()->isServiceRegistered(
185 QStringLiteral("com.canonical.indicator.application"));
186 }
187 if (!necessary) {
188 necessary = session.interface()->isServiceRegistered(
189 QStringLiteral("org.ayatana.indicator.application"));
190 }
191 if (!necessary && QGuiApplication::desktopSettingsAware()) {
192 // Accessing to process name might be not allowed if the application
193 // is confined, thus we can just rely on the current desktop in use
194 const QPlatformServices *services = QGuiApplicationPrivate::platformIntegration()->services();
195 if (services)
196 necessary = services->desktopEnvironment().split(':').contains("UNITY");
197 }
198 necessity_checked = true;
199 }
200 if (!necessary)
201 return nullptr;
202 QTemporaryFile *ret = new QTemporaryFile(tempFileTemplate(), this);
203 if (!ret->open()) {
204 delete ret;
205 return nullptr;
206 }
207 icon.pixmap(QSize(22, 22)).save(ret);
208 ret->close();
209 return ret;
210}
211
213{
214 if (!m_dbusConnection) {
215 m_dbusConnection = new QDBusMenuConnection(this, m_instanceId);
216 m_notifier = new QXdgNotificationInterface(XdgNotificationService,
217 XdgNotificationPath, m_dbusConnection->connection(), this);
218 connect(m_notifier, SIGNAL(NotificationClosed(uint,uint)), this, SLOT(notificationClosed(uint,uint)));
219 connect(m_notifier, SIGNAL(ActionInvoked(uint,QString)), this, SLOT(actionInvoked(uint,QString)));
220 }
221 return m_dbusConnection;
222}
223
224void QDBusTrayIcon::updateIcon(const QIcon &icon)
225{
226 m_iconName = icon.name();
227 m_icon = icon;
228 if (m_iconName.isEmpty()) {
229 if (m_tempIcon)
230 delete m_tempIcon;
231 m_tempIcon = tempIcon(icon);
232 if (m_tempIcon)
233 m_iconName = m_tempIcon->fileName();
234 }
235 qCDebug(qLcTray) << m_iconName << icon.availableSizes();
236 emit iconChanged();
237}
238
239void QDBusTrayIcon::updateToolTip(const QString &tooltip)
240{
241 qCDebug(qLcTray) << tooltip;
242 m_tooltip = tooltip;
243 emit tooltipChanged();
244}
245
247{
248 return new QDBusPlatformMenu();
249}
250
251void QDBusTrayIcon::updateMenu(QPlatformMenu * menu)
252{
253 qCDebug(qLcTray) << menu;
254 QDBusPlatformMenu *newMenu = qobject_cast<QDBusPlatformMenu *>(menu);
255 if (m_menu != newMenu) {
256 if (m_menu) {
257 dBusConnection()->unregisterTrayIconMenu(this);
258 delete m_menuAdaptor;
259 }
260 m_menu = newMenu;
261 m_menuAdaptor = new QDBusMenuAdaptor(m_menu);
262 // TODO connect(m_menu, , m_menuAdaptor, SIGNAL(ItemActivationRequested(int,uint)));
263 connect(m_menu, SIGNAL(propertiesUpdated(QDBusMenuItemList,QDBusMenuItemKeysList)),
264 m_menuAdaptor, SIGNAL(ItemsPropertiesUpdated(QDBusMenuItemList,QDBusMenuItemKeysList)));
265 connect(m_menu, SIGNAL(updated(uint,int)),
266 m_menuAdaptor, SIGNAL(LayoutUpdated(uint,int)));
267 dBusConnection()->registerTrayIconMenu(this);
268 emit menuChanged();
269 }
270}
271
272void QDBusTrayIcon::showMessage(const QString &title, const QString &msg, const QIcon &icon,
273 QPlatformSystemTrayIcon::MessageIcon iconType, int msecs)
274{
275 m_messageTitle = title;
276 m_message = msg;
277 m_attentionIcon = icon;
278 QStringList notificationActions;
279 switch (iconType) {
280 case Information:
281 m_attentionIconName = QStringLiteral("dialog-information");
282 break;
283 case Warning:
284 m_attentionIconName = QStringLiteral("dialog-warning");
285 break;
286 case Critical:
287 m_attentionIconName = QStringLiteral("dialog-error");
288 // If there are actions, the desktop notification may appear as a message dialog
289 // with button(s), which will interrupt the user and require a response.
290 // That is an optional feature in implementations of org.freedesktop.Notifications
291 notificationActions << DefaultAction << tr("OK");
292 break;
293 default:
294 m_attentionIconName.clear();
295 break;
296 }
297 if (m_attentionIconName.isEmpty()) {
298 if (m_tempAttentionIcon)
299 delete m_tempAttentionIcon;
300 m_tempAttentionIcon = tempIcon(icon);
301 if (m_tempAttentionIcon)
302 m_attentionIconName = m_tempAttentionIcon->fileName();
303 }
304 qCDebug(qLcTray) << title << msg <<
305 QPlatformSystemTrayIcon::metaObject()->enumerator(
306 QPlatformSystemTrayIcon::staticMetaObject.indexOfEnumerator("MessageIcon")).valueToKey(iconType)
307 << m_attentionIconName << msecs;
308 setStatus(QStringLiteral("NeedsAttention"));
309 m_attentionTimer.start(msecs);
310 emit tooltipChanged();
311 emit attention();
312
313 // Desktop notification
314 QVariantMap hints;
315 // urgency levels according to https://developer.gnome.org/notification-spec/#urgency-levels
316 // 0 low, 1 normal, 2 critical
317 int urgency = static_cast<int>(iconType) - 1;
318 if (urgency < 0) // no icon
319 urgency = 0;
320 hints.insert("urgency"_L1, QVariant(urgency));
321 m_notifier->notify(QCoreApplication::applicationName(), 0,
322 m_attentionIconName, title, msg, notificationActions, hints, msecs);
323}
324
325void QDBusTrayIcon::actionInvoked(uint id, const QString &action)
326{
327 qCDebug(qLcTray) << id << action;
328 emit messageClicked();
329}
330
331void QDBusTrayIcon::notificationClosed(uint id, uint reason)
332{
333 qCDebug(qLcTray) << id << reason;
334}
335
337{
338 QDBusMenuConnection * conn = const_cast<QDBusTrayIcon *>(this)->dBusConnection();
339
340 // If the KDE watcher service is registered, we must be on a desktop
341 // where a StatusNotifier-conforming system tray exists.
342 qCDebug(qLcTray) << conn->isWatcherRegistered();
343 return conn->isWatcherRegistered();
344}
345
346QT_END_NAMESPACE
347
348#include "moc_qdbustrayicon_p.cpp"
349#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...
static const QString KDEWatcherService
static int instanceCount
static const QString DefaultAction
static const QString KDEItemFormat
static QString tempFileTemplate()
static const QString XdgNotificationPath
static QString iconTempPath()
static const QString XdgNotificationService
Q_LOGGING_CATEGORY(lcEventDispatcher, "qt.eventdispatcher")