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