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 if (!ret->open()) {
202 delete ret;
203 return nullptr;
204 }
205 icon.pixmap(size: QSize(22, 22)).save(device: ret);
206 ret->close();
207 return ret;
208}
209
210QDBusMenuConnection * QDBusTrayIcon::dBusConnection()
211{
212 if (!m_dbusConnection) {
213 m_dbusConnection = new QDBusMenuConnection(this, m_instanceId);
214 m_notifier = new QXdgNotificationInterface(XdgNotificationService,
215 XdgNotificationPath, m_dbusConnection->connection(), this);
216 connect(sender: m_notifier, SIGNAL(NotificationClosed(uint,uint)), receiver: this, SLOT(notificationClosed(uint,uint)));
217 connect(sender: m_notifier, SIGNAL(ActionInvoked(uint,QString)), receiver: this, SLOT(actionInvoked(uint,QString)));
218 }
219 return m_dbusConnection;
220}
221
222void QDBusTrayIcon::updateIcon(const QIcon &icon)
223{
224 m_iconName = icon.name();
225 m_icon = icon;
226 if (m_iconName.isEmpty()) {
227 if (m_tempIcon)
228 delete m_tempIcon;
229 m_tempIcon = tempIcon(icon);
230 if (m_tempIcon)
231 m_iconName = m_tempIcon->fileName();
232 }
233 qCDebug(qLcTray) << m_iconName << icon.availableSizes();
234 emit iconChanged();
235}
236
237void QDBusTrayIcon::updateToolTip(const QString &tooltip)
238{
239 qCDebug(qLcTray) << tooltip;
240 m_tooltip = tooltip;
241 emit tooltipChanged();
242}
243
244QPlatformMenu *QDBusTrayIcon::createMenu() const
245{
246 return new QDBusPlatformMenu();
247}
248
249void QDBusTrayIcon::updateMenu(QPlatformMenu * menu)
250{
251 qCDebug(qLcTray) << menu;
252 QDBusPlatformMenu *newMenu = qobject_cast<QDBusPlatformMenu *>(object: menu);
253 if (m_menu != newMenu) {
254 if (m_menu) {
255 dBusConnection()->unregisterTrayIconMenu(item: this);
256 delete m_menuAdaptor;
257 }
258 m_menu = newMenu;
259 m_menuAdaptor = new QDBusMenuAdaptor(m_menu);
260 // TODO connect(m_menu, , m_menuAdaptor, SIGNAL(ItemActivationRequested(int,uint)));
261 connect(sender: m_menu, SIGNAL(propertiesUpdated(QDBusMenuItemList,QDBusMenuItemKeysList)),
262 receiver: m_menuAdaptor, SIGNAL(ItemsPropertiesUpdated(QDBusMenuItemList,QDBusMenuItemKeysList)));
263 connect(sender: m_menu, SIGNAL(updated(uint,int)),
264 receiver: m_menuAdaptor, SIGNAL(LayoutUpdated(uint,int)));
265 dBusConnection()->registerTrayIconMenu(item: this);
266 emit menuChanged();
267 }
268}
269
270void QDBusTrayIcon::showMessage(const QString &title, const QString &msg, const QIcon &icon,
271 QPlatformSystemTrayIcon::MessageIcon iconType, int msecs)
272{
273 m_messageTitle = title;
274 m_message = msg;
275 m_attentionIcon = icon;
276 QStringList notificationActions;
277 switch (iconType) {
278 case Information:
279 m_attentionIconName = QStringLiteral("dialog-information");
280 break;
281 case Warning:
282 m_attentionIconName = QStringLiteral("dialog-warning");
283 break;
284 case Critical:
285 m_attentionIconName = QStringLiteral("dialog-error");
286 // If there are actions, the desktop notification may appear as a message dialog
287 // with button(s), which will interrupt the user and require a response.
288 // That is an optional feature in implementations of org.freedesktop.Notifications
289 notificationActions << DefaultAction << tr(s: "OK");
290 break;
291 default:
292 m_attentionIconName.clear();
293 break;
294 }
295 if (m_attentionIconName.isEmpty()) {
296 if (m_tempAttentionIcon)
297 delete m_tempAttentionIcon;
298 m_tempAttentionIcon = tempIcon(icon);
299 if (m_tempAttentionIcon)
300 m_attentionIconName = m_tempAttentionIcon->fileName();
301 }
302 qCDebug(qLcTray) << title << msg <<
303 QPlatformSystemTrayIcon::metaObject()->enumerator(
304 index: QPlatformSystemTrayIcon::staticMetaObject.indexOfEnumerator(name: "MessageIcon")).valueToKey(value: iconType)
305 << m_attentionIconName << msecs;
306 setStatus(QStringLiteral("NeedsAttention"));
307 m_attentionTimer.start(msec: msecs);
308 emit tooltipChanged();
309 emit attention();
310
311 // Desktop notification
312 QVariantMap hints;
313 // urgency levels according to https://developer.gnome.org/notification-spec/#urgency-levels
314 // 0 low, 1 normal, 2 critical
315 int urgency = static_cast<int>(iconType) - 1;
316 if (urgency < 0) // no icon
317 urgency = 0;
318 hints.insert(key: "urgency"_L1, value: QVariant(urgency));
319 m_notifier->notify(appName: QCoreApplication::applicationName(), replacesId: 0,
320 appIcon: m_attentionIconName, summary: title, body: msg, actions: notificationActions, hints, timeout: msecs);
321}
322
323void QDBusTrayIcon::actionInvoked(uint id, const QString &action)
324{
325 qCDebug(qLcTray) << id << action;
326 emit messageClicked();
327}
328
329void QDBusTrayIcon::notificationClosed(uint id, uint reason)
330{
331 qCDebug(qLcTray) << id << reason;
332}
333
334bool QDBusTrayIcon::isSystemTrayAvailable() const
335{
336 QDBusMenuConnection * conn = const_cast<QDBusTrayIcon *>(this)->dBusConnection();
337
338 // If the KDE watcher service is registered, we must be on a desktop
339 // where a StatusNotifier-conforming system tray exists.
340 qCDebug(qLcTray) << conn->isWatcherRegistered();
341 return conn->isWatcherRegistered();
342}
343
344QT_END_NAMESPACE
345
346#include "moc_qdbustrayicon_p.cpp"
347#endif //QT_NO_SYSTEMTRAYICON
348

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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