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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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