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

source code of qtbase/src/gui/platform/unix/dbustray/qdbustrayicon.cpp