clang-tidy: Avoid another warning about potential memory leak
[quassel.git] / src / qtui / statusnotifieritem.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This contains code from KStatusNotifierItem, part of the KDE libs     *
6  *   Copyright (C) 2009 Marco Martin <notmart@gmail.com>                   *
7  *                                                                         *
8  *   This file is free software; you can redistribute it and/or modify     *
9  *   it under the terms of the GNU Library General Public License (LGPL)   *
10  *   as published by the Free Software Foundation; either version 2 of the *
11  *   License, or (at your option) any later version.                       *
12  *                                                                         *
13  *   This program is distributed in the hope that it will be useful,       *
14  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
15  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
16  *   GNU General Public License for more details.                          *
17  *                                                                         *
18  *   You should have received a copy of the GNU General Public License     *
19  *   along with this program; if not, write to the                         *
20  *   Free Software Foundation, Inc.,                                       *
21  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
22  ***************************************************************************/
23
24 #ifdef HAVE_DBUS
25
26 #include "statusnotifieritem.h"
27
28 #include <QApplication>
29 #include <QDir>
30 #include <QFile>
31 #include <QIcon>
32 #include <QMenu>
33 #include <QMouseEvent>
34 #include <QTextDocument>
35
36 #include "icon.h"
37 #include "qtui.h"
38 #include "quassel.h"
39 #include "statusnotifieritemdbus.h"
40
41 constexpr int kProtocolVersion {0};
42
43 const QString kSniWatcherService       {QLatin1String{"org.kde.StatusNotifierWatcher"}};
44 const QString kSniWatcherPath          {QLatin1String{"/StatusNotifierWatcher"}};
45 const QString kSniPath                 {QLatin1String{"/StatusNotifierItem"}};
46 const QString kXdgNotificationsService {QLatin1String{"org.freedesktop.Notifications"}};
47 const QString kXdgNotificationsPath    {QLatin1String{"/org/freedesktop/Notifications"}};
48 const QString kMenuObjectPath          {QLatin1String{"/MenuBar"}};
49
50 #ifdef HAVE_DBUSMENU
51 #  include "dbusmenuexporter.h"
52
53 /**
54  * Specialization to provide access to icon names
55  */
56 class QuasselDBusMenuExporter : public DBusMenuExporter
57 {
58 public:
59     QuasselDBusMenuExporter(const QString &dbusObjectPath, QMenu *menu, const QDBusConnection &dbusConnection)
60         : DBusMenuExporter(dbusObjectPath, menu, dbusConnection)
61     {}
62
63 protected:
64     virtual QString iconNameForAction(QAction *action) // TODO Qt 4.7: fixme when we have converted our iconloader
65     {
66         QIcon icon(action->icon());
67         return icon.isNull() ? QString() : icon.name();
68     }
69 };
70
71 #endif /* HAVE_DBUSMENU */
72
73 StatusNotifierItem::StatusNotifierItem(QWidget *parent)
74     : StatusNotifierItemParent(parent)
75 #if QT_VERSION >= 0x050000
76     , _iconThemeDir{QDir::tempPath() + QLatin1String{"/quassel-sni-XXXXXX"}}
77 #endif
78 {
79     static bool registered = []() -> bool {
80         qDBusRegisterMetaType<DBusImageStruct>();
81         qDBusRegisterMetaType<DBusImageVector>();
82         qDBusRegisterMetaType<DBusToolTipStruct>();
83         return true;
84     }();
85     Q_UNUSED(registered)
86
87     setMode(Mode::StatusNotifier);
88
89     connect(this, SIGNAL(visibilityChanged(bool)), this, SLOT(onVisibilityChanged(bool)));
90     connect(this, SIGNAL(modeChanged(Mode)), this, SLOT(onModeChanged(Mode)));
91     connect(this, SIGNAL(stateChanged(State)), this, SLOT(onStateChanged(State)));
92
93     trayMenu()->installEventFilter(this);
94
95     // Create a temporary directory that holds copies of the tray icons. That way, visualizers can find our icons.
96     // For Qt4 the relevant icons are installed in hicolor already, so nothing to be done.
97 #if QT_VERSION >= 0x050000
98     if (_iconThemeDir.isValid()) {
99         _iconThemePath = _iconThemeDir.path();
100     }
101     else {
102         qWarning() << "Could not create temporary directory for themed tray icons!";
103     }
104 #endif
105
106     connect(this, SIGNAL(iconsChanged()), this, SLOT(refreshIcons()));
107     refreshIcons();
108
109     // Our own SNI service
110     _statusNotifierItemDBus = new StatusNotifierItemDBus(this);
111     connect(this, SIGNAL(currentIconNameChanged()), _statusNotifierItemDBus, SIGNAL(NewIcon()));
112     connect(this, SIGNAL(currentIconNameChanged()), _statusNotifierItemDBus, SIGNAL(NewAttentionIcon()));
113     connect(this, SIGNAL(toolTipChanged(QString, QString)), _statusNotifierItemDBus, SIGNAL(NewToolTip()));
114
115     // Service watcher to keep track of the StatusNotifierWatcher service
116     QDBusServiceWatcher *watcher = new QDBusServiceWatcher(kSniWatcherService,
117                                                            QDBusConnection::sessionBus(),
118                                                            QDBusServiceWatcher::WatchForOwnerChange,
119                                                            this);
120     connect(watcher, SIGNAL(serviceOwnerChanged(QString, QString, QString)), SLOT(serviceChange(QString, QString, QString)));
121
122     // Client instance for StatusNotifierWatcher
123     _statusNotifierWatcher = new org::kde::StatusNotifierWatcher(kSniWatcherService,
124                                                                  kSniWatcherPath,
125                                                                  QDBusConnection::sessionBus(),
126                                                                  this);
127     connect(_statusNotifierWatcher, SIGNAL(StatusNotifierHostRegistered()), SLOT(checkForRegisteredHosts()));
128     connect(_statusNotifierWatcher, SIGNAL(StatusNotifierHostUnregistered()), SLOT(checkForRegisteredHosts()));
129
130     // Client instance for notifications
131     _notificationsClient = new org::freedesktop::Notifications(kXdgNotificationsService,
132                                                                kXdgNotificationsPath,
133                                                                QDBusConnection::sessionBus(),
134                                                                this);
135     connect(_notificationsClient, SIGNAL(NotificationClosed(uint, uint)), SLOT(notificationClosed(uint, uint)));
136     connect(_notificationsClient, SIGNAL(ActionInvoked(uint, QString)), SLOT(notificationInvoked(uint, QString)));
137
138     if (_notificationsClient->isValid()) {
139         QStringList desktopCapabilities = _notificationsClient->GetCapabilities();
140         _notificationsClientSupportsMarkup = desktopCapabilities.contains("body-markup");
141         _notificationsClientSupportsActions = desktopCapabilities.contains("actions");
142     }
143
144 #ifdef HAVE_DBUSMENU
145     new QuasselDBusMenuExporter(menuObjectPath(), trayMenu(), _statusNotifierItemDBus->dbusConnection()); // will be added as menu child
146 #endif
147 }
148
149
150 void StatusNotifierItem::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner)
151 {
152     Q_UNUSED(name);
153     if (newOwner.isEmpty()) {
154         //unregistered
155         setMode(Mode::Legacy);
156     }
157     else if (oldOwner.isEmpty()) {
158         //registered
159         setMode(Mode::StatusNotifier);
160     }
161 }
162
163
164 void StatusNotifierItem::registerToWatcher()
165 {
166     if (_statusNotifierWatcher->isValid() && _statusNotifierWatcher->property("ProtocolVersion").toInt() == kProtocolVersion) {
167         auto registerMethod = QDBusMessage::createMethodCall(kSniWatcherService, kSniWatcherPath, kSniWatcherService,
168                                                              QLatin1String{"RegisterStatusNotifierItem"});
169         registerMethod.setArguments(QVariantList() << _statusNotifierItemDBus->service());
170         _statusNotifierItemDBus->dbusConnection().callWithCallback(registerMethod, this, SLOT(checkForRegisteredHosts()), SLOT(onDBusError(QDBusError)));
171     }
172     else {
173         setMode(Mode::Legacy);
174     }
175 }
176
177
178 void StatusNotifierItem::checkForRegisteredHosts()
179 {
180     if (!_statusNotifierWatcher || !_statusNotifierWatcher->property("IsStatusNotifierHostRegistered").toBool()) {
181         setMode(Mode::Legacy);
182     }
183     else {
184         setMode(Mode::StatusNotifier);
185     }
186 }
187
188
189 void StatusNotifierItem::onDBusError(const QDBusError &error)
190 {
191     qWarning() << "StatusNotifierItem encountered a D-Bus error:" << error;
192     setMode(Mode::Legacy);
193 }
194
195
196 void StatusNotifierItem::refreshIcons()
197 {
198 #if QT_VERSION >= 0x050000
199     if (!_iconThemePath.isEmpty()) {
200         QDir baseDir{_iconThemePath + "/hicolor"};
201         baseDir.removeRecursively();
202         for (auto &&trayState : { State::Active, State::Passive, State::NeedsAttention }) {
203             auto iconName = SystemTray::iconName(trayState);
204             QIcon icon = icon::get(iconName);
205             if (!icon.isNull()) {
206                 for (auto &&size : icon.availableSizes()) {
207                     auto pixDir = QString{"%1/%2x%3/status"}.arg(baseDir.absolutePath()).arg(size.width()).arg(size.height());
208                     QDir{}.mkpath(pixDir);
209                     if (!icon.pixmap(size).save(pixDir + "/" + iconName + ".png")) {
210                         qWarning() << "Could not save tray icon" << iconName << "for size" << size;
211                     }
212                 }
213             }
214             else {
215                 // No theme icon found; use fallback from resources
216                 auto iconDir = QString{"%1/24x24/status"}.arg(baseDir.absolutePath());
217                 QDir{}.mkpath(iconDir);
218                 if (!QFile::copy(QString{":/icons/hicolor/24x24/status/%1.svg"}.arg(iconName),
219                                  QString{"%1/%2.svg"}.arg(iconDir, iconName))) {
220                     qWarning() << "Could not access fallback tray icon" << iconName;
221                     continue;
222                 }
223             }
224         }
225     }
226 #endif
227     if (_statusNotifierItemDBus) {
228         emit _statusNotifierItemDBus->NewIcon();
229         emit _statusNotifierItemDBus->NewAttentionIcon();
230     }
231 }
232
233
234 bool StatusNotifierItem::isSystemTrayAvailable() const
235 {
236     if (mode() == Mode::StatusNotifier) {
237         return true;  // else it should be set to legacy on registration
238     }
239
240     return StatusNotifierItemParent::isSystemTrayAvailable();
241 }
242
243
244 void StatusNotifierItem::onModeChanged(Mode mode)
245 {
246     if (mode == Mode::StatusNotifier) {
247         _statusNotifierItemDBus->registerTrayIcon();
248         registerToWatcher();
249     }
250     else {
251         _statusNotifierItemDBus->unregisterTrayIcon();
252     }
253 }
254
255
256 void StatusNotifierItem::onStateChanged(State state)
257 {
258     if (mode() == Mode::StatusNotifier) {
259         emit _statusNotifierItemDBus->NewStatus(metaObject()->enumerator(metaObject()->indexOfEnumerator("State")).valueToKey(state));
260     }
261 }
262
263
264 void StatusNotifierItem::onVisibilityChanged(bool isVisible)
265 {
266     if (mode() == Mode::StatusNotifier) {
267         if (isVisible) {
268             _statusNotifierItemDBus->registerTrayIcon();
269             registerToWatcher();
270         }
271         else {
272             _statusNotifierItemDBus->unregisterTrayIcon();
273         }
274     }
275 }
276
277
278 QString StatusNotifierItem::title() const
279 {
280     return QString("Quassel IRC");
281 }
282
283
284 QString StatusNotifierItem::iconName() const
285 {
286     return currentIconName();
287 }
288
289
290 QString StatusNotifierItem::attentionIconName() const
291 {
292     return currentAttentionIconName();
293 }
294
295
296 QString StatusNotifierItem::toolTipIconName() const
297 {
298     return "quassel";
299 }
300
301
302 QString StatusNotifierItem::iconThemePath() const
303 {
304     return _iconThemePath;
305 }
306
307
308 QString StatusNotifierItem::menuObjectPath() const
309 {
310     return kMenuObjectPath;
311 }
312
313
314 void StatusNotifierItem::activated(const QPoint &pos)
315 {
316     Q_UNUSED(pos)
317     activate(Trigger);
318 }
319
320
321 bool StatusNotifierItem::eventFilter(QObject *watched, QEvent *event)
322 {
323     if (mode() == StatusNotifier) {
324         //FIXME: ugly ugly workaround to weird QMenu's focus problems
325 #ifdef HAVE_KDE4
326         if (watched == trayMenu() &&
327             (event->type() == QEvent::WindowDeactivate || (event->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton))) {
328             // put at the back of event queue to let the action activate anyways
329             QTimer::singleShot(0, trayMenu(), SLOT(hide()));
330         }
331 #else
332         if (watched == trayMenu() && event->type() == QEvent::HoverLeave) {
333             trayMenu()->hide();
334         }
335 #endif
336     }
337     return StatusNotifierItemParent::eventFilter(watched, event);
338 }
339
340
341 void StatusNotifierItem::showMessage(const QString &title, const QString &message_, SystemTray::MessageIcon icon, int timeout, uint notificationId)
342 {
343     QString message = message_;
344     if (_notificationsClient->isValid()) {
345         if (_notificationsClientSupportsMarkup)
346 #if QT_VERSION < 0x050000
347             message = Qt::escape(message);
348 #else
349             message = message.toHtmlEscaped();
350 #endif
351
352         QStringList actions;
353         if (_notificationsClientSupportsActions)
354             actions << "activate" << "View";
355
356         // we always queue notifications right now
357         QDBusReply<uint> reply = _notificationsClient->Notify(title, 0, "quassel", title, message, actions, QVariantMap(), timeout);
358         if (reply.isValid()) {
359             uint dbusid = reply.value();
360             _notificationsIdMap.insert(dbusid, notificationId);
361             _lastNotificationsDBusId = dbusid;
362         }
363     }
364     else
365         StatusNotifierItemParent::showMessage(title, message, icon, timeout, notificationId);
366 }
367
368
369 void StatusNotifierItem::closeMessage(uint notificationId)
370 {
371     for (auto &&dbusid : _notificationsIdMap.keys()) {
372         if (_notificationsIdMap.value(dbusid) == notificationId) {
373             _notificationsIdMap.remove(dbusid);
374             _notificationsClient->CloseNotification(dbusid);
375         }
376     }
377     _lastNotificationsDBusId = 0;
378
379     StatusNotifierItemParent::closeMessage(notificationId);
380 }
381
382
383 void StatusNotifierItem::notificationClosed(uint dbusid, uint reason)
384 {
385     Q_UNUSED(reason)
386     _lastNotificationsDBusId = 0;
387     emit messageClosed(_notificationsIdMap.take(dbusid));
388 }
389
390
391 void StatusNotifierItem::notificationInvoked(uint dbusid, const QString &action)
392 {
393     Q_UNUSED(action)
394     emit messageClicked(_notificationsIdMap.value(dbusid, 0));
395 }
396
397
398 #endif