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 "qdbusmenuconnection_p.h"
45 #include "qstatusnotifieritemadaptor_p.h"
46 #include "qdbusmenuadaptor_p.h"
47 #include "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 
67 QT_BEGIN_NAMESPACE
68 
69 Q_LOGGING_CATEGORY(qLcTray, "qt.qpa.tray")
70 
iconTempPath()71 static 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 
94 static const QString KDEItemFormat = QStringLiteral("org.kde.StatusNotifierItem-%1-%2");
95 static const QString KDEWatcherService = QStringLiteral("org.kde.StatusNotifierWatcher");
96 static const QString XdgNotificationService = QStringLiteral("org.freedesktop.Notifications");
97 static const QString XdgNotificationPath = QStringLiteral("/org/freedesktop/Notifications");
98 static const QString DefaultAction = QStringLiteral("default");
99 static int instanceCount = 0;
100 
tempFileTemplate()101 static 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 
QDBusTrayIcon()112 QDBusTrayIcon::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 
~QDBusTrayIcon()143 QDBusTrayIcon::~QDBusTrayIcon()
144 {
145 }
146 
init()147 void 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 
cleanup()155 void 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 
watcherServiceRegistered(const QString & serviceName)167 void 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 
attentionTimerExpired()176 void 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 
setStatus(const QString & status)186 void 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 
tempIcon(const QIcon & icon)195 QTemporaryFile *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 
dBusConnection()223 QDBusMenuConnection * 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 
updateIcon(const QIcon & icon)235 void 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 
updateToolTip(const QString & tooltip)250 void QDBusTrayIcon::updateToolTip(const QString &tooltip)
251 {
252     qCDebug(qLcTray) << tooltip;
253     m_tooltip = tooltip;
254     emit tooltipChanged();
255 }
256 
createMenu() const257 QPlatformMenu *QDBusTrayIcon::createMenu() const
258 {
259     return new QDBusPlatformMenu();
260 }
261 
updateMenu(QPlatformMenu * menu)262 void 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 
showMessage(const QString & title,const QString & msg,const QIcon & icon,QPlatformSystemTrayIcon::MessageIcon iconType,int msecs)283 void 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 
actionInvoked(uint id,const QString & action)336 void QDBusTrayIcon::actionInvoked(uint id, const QString &action)
337 {
338     qCDebug(qLcTray) << id << action;
339     emit messageClicked();
340 }
341 
notificationClosed(uint id,uint reason)342 void QDBusTrayIcon::notificationClosed(uint id, uint reason)
343 {
344     qCDebug(qLcTray) << id << reason;
345 }
346 
isSystemTrayAvailable() const347 bool QDBusTrayIcon::isSystemTrayAvailable() const
348 {
349     QDBusMenuConnection * conn = const_cast<QDBusTrayIcon *>(this)->dBusConnection();
350     qCDebug(qLcTray) << conn->isStatusNotifierHostRegistered();
351     return conn->isStatusNotifierHostRegistered();
352 }
353 
354 QT_END_NAMESPACE
355 #endif //QT_NO_SYSTEMTRAYICON
356