qtui: Clean up SystemTray
[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 <QApplication>
27 #include <QDir>
28 #include <QMenu>
29 #include <QMouseEvent>
30 #include <QTextDocument>
31
32 #include "qtui.h"
33 #include "quassel.h"
34 #include "statusnotifieritem.h"
35 #include "statusnotifieritemdbus.h"
36
37 const int StatusNotifierItem::_protocolVersion = 0;
38 const QString StatusNotifierItem::_statusNotifierWatcherServiceName("org.kde.StatusNotifierWatcher");
39
40 #ifdef HAVE_DBUSMENU
41 #  include "dbusmenuexporter.h"
42
43 /**
44  * Specialization to provide access to icon names
45  */
46 class QuasselDBusMenuExporter : public DBusMenuExporter
47 {
48 public:
49     QuasselDBusMenuExporter(const QString &dbusObjectPath, QMenu *menu, const QDBusConnection &dbusConnection)
50         : DBusMenuExporter(dbusObjectPath, menu, dbusConnection)
51     {}
52
53 protected:
54     virtual QString iconNameForAction(QAction *action) // TODO Qt 4.7: fixme when we have converted our iconloader
55     {
56         QIcon icon(action->icon());
57         return icon.isNull() ? QString() : icon.name();
58     }
59 };
60
61 #endif /* HAVE_DBUSMENU */
62
63 StatusNotifierItem::StatusNotifierItem(QWidget *parent)
64     : StatusNotifierItemParent(parent)
65 #if QT_VERSION >= 0x050000
66     , _iconThemeDir{QDir::tempPath() + QLatin1String{"/quassel-sni-XXXXXX"}}
67 #endif
68 {
69     // Create a temporary directory that holds copies of the tray icons. That way, visualizers can find our icons.
70     // For Qt4 the relevant icons are installed in hicolor already, so nothing to be done.
71 #if QT_VERSION >= 0x050000
72     if (_iconThemeDir.isValid()) {
73         _iconThemePath = _iconThemeDir.path();
74     }
75     else {
76         qWarning() << StatusNotifierItem::tr("Could not create temporary directory for themed tray icons: %1").arg(_iconThemeDir.errorString());
77     }
78 #endif
79
80     connect(QtUi::instance(), SIGNAL(iconThemeRefreshed()), this, SLOT(refreshIcons()));
81 }
82
83
84 StatusNotifierItem::~StatusNotifierItem()
85 {
86     delete _statusNotifierWatcher;
87 }
88
89
90 void StatusNotifierItem::init()
91 {
92     qDBusRegisterMetaType<DBusImageStruct>();
93     qDBusRegisterMetaType<DBusImageVector>();
94     qDBusRegisterMetaType<DBusToolTipStruct>();
95
96     refreshIcons();
97
98     _statusNotifierItemDBus = new StatusNotifierItemDBus(this);
99
100     connect(this, SIGNAL(toolTipChanged(QString, QString)), _statusNotifierItemDBus, SIGNAL(NewToolTip()));
101     connect(this, SIGNAL(animationEnabledChanged(bool)), _statusNotifierItemDBus, SIGNAL(NewAttentionIcon()));
102
103     QDBusServiceWatcher *watcher = new QDBusServiceWatcher(_statusNotifierWatcherServiceName,
104         QDBusConnection::sessionBus(),
105         QDBusServiceWatcher::WatchForOwnerChange,
106         this);
107     connect(watcher, SIGNAL(serviceOwnerChanged(QString, QString, QString)), SLOT(serviceChange(QString, QString, QString)));
108
109     setMode(StatusNotifier);
110
111     _notificationsClient = new org::freedesktop::Notifications("org.freedesktop.Notifications", "/org/freedesktop/Notifications",
112         QDBusConnection::sessionBus(), this);
113
114     connect(_notificationsClient, SIGNAL(NotificationClosed(uint, uint)), SLOT(notificationClosed(uint, uint)));
115     connect(_notificationsClient, SIGNAL(ActionInvoked(uint, QString)), SLOT(notificationInvoked(uint, QString)));
116
117     if (_notificationsClient->isValid()) {
118         QStringList desktopCapabilities = _notificationsClient->GetCapabilities();
119         _notificationsClientSupportsMarkup = desktopCapabilities.contains("body-markup");
120         _notificationsClientSupportsActions = desktopCapabilities.contains("actions");
121     }
122
123     StatusNotifierItemParent::init();
124     trayMenu()->installEventFilter(this);
125
126 #ifdef HAVE_DBUSMENU
127     _menuObjectPath = "/MenuBar";
128     new QuasselDBusMenuExporter(menuObjectPath(), trayMenu(), _statusNotifierItemDBus->dbusConnection()); // will be added as menu child
129 #endif
130 }
131
132
133 void StatusNotifierItem::registerToDaemon()
134 {
135     if (!_statusNotifierWatcher) {
136         _statusNotifierWatcher = new org::kde::StatusNotifierWatcher(_statusNotifierWatcherServiceName,
137             "/StatusNotifierWatcher",
138             QDBusConnection::sessionBus());
139         connect(_statusNotifierWatcher, SIGNAL(StatusNotifierHostRegistered()), SLOT(checkForRegisteredHosts()));
140         connect(_statusNotifierWatcher, SIGNAL(StatusNotifierHostUnregistered()), SLOT(checkForRegisteredHosts()));
141     }
142     if (_statusNotifierWatcher->isValid()
143         && _statusNotifierWatcher->property("ProtocolVersion").toInt() == _protocolVersion) {
144         _statusNotifierWatcher->RegisterStatusNotifierItem(_statusNotifierItemDBus->service());
145         checkForRegisteredHosts();
146     }
147     else {
148         //qDebug() << "StatusNotifierWatcher not reachable!";
149         setMode(Legacy);
150     }
151 }
152
153
154 void StatusNotifierItem::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner)
155 {
156     Q_UNUSED(name);
157     if (newOwner.isEmpty()) {
158         //unregistered
159         //qDebug() << "Connection to the StatusNotifierWatcher lost";
160         delete _statusNotifierWatcher;
161         _statusNotifierWatcher = nullptr;
162         setMode(Legacy);
163     }
164     else if (oldOwner.isEmpty()) {
165         //registered
166         setMode(StatusNotifier);
167     }
168 }
169
170
171 void StatusNotifierItem::checkForRegisteredHosts()
172 {
173     if (!_statusNotifierWatcher || !_statusNotifierWatcher->property("IsStatusNotifierHostRegistered").toBool())
174         setMode(Legacy);
175     else
176         setMode(StatusNotifier);
177 }
178
179
180 void StatusNotifierItem::refreshIcons()
181 {
182 #if QT_VERSION >= 0x050000
183     if (!_iconThemePath.isEmpty()) {
184         QDir baseDir{_iconThemePath + "/hicolor"};
185         baseDir.removeRecursively();
186         for (auto &&trayState : { State::Active, State::Passive, State::NeedsAttention }) {
187             const QIcon &icon = SystemTray::stateIcon(trayState);
188             if (!icon.name().isEmpty()) {
189                 for (auto &&size : icon.availableSizes()) {
190                     auto pixDir = QString{"%1/%2x%3/status"}.arg(baseDir.absolutePath()).arg(size.width()).arg(size.height());
191                     QDir{}.mkpath(pixDir);
192                     if (!icon.pixmap(size).save(pixDir + "/" + icon.name() + ".png")) {
193                         qWarning() << "Could not save tray icon" << icon.name() << "for size" << size;
194                     }
195                 }
196             }
197         }
198     }
199 #endif
200     if (_statusNotifierItemDBus) {
201         emit _statusNotifierItemDBus->NewIcon();
202         emit _statusNotifierItemDBus->NewAttentionIcon();
203     }
204 }
205
206
207 bool StatusNotifierItem::isSystemTrayAvailable() const
208 {
209     if (mode() == StatusNotifier)
210         return true;  // else it should be set to legacy on registration
211
212     return StatusNotifierItemParent::isSystemTrayAvailable();
213 }
214
215
216 bool StatusNotifierItem::isVisible() const
217 {
218     if (mode() == StatusNotifier)
219         return shouldBeVisible();  // we don't have a way to check, so we need to trust everything went right
220
221     return StatusNotifierItemParent::isVisible();
222 }
223
224
225 void StatusNotifierItem::setMode(Mode mode_)
226 {
227     if (mode_ == mode())
228         return;
229
230     if (mode_ != StatusNotifier) {
231         _statusNotifierItemDBus->unregisterService();
232     }
233
234     StatusNotifierItemParent::setMode(mode_);
235
236     if (mode() == StatusNotifier) {
237         _statusNotifierItemDBus->registerService();
238         registerToDaemon();
239     }
240 }
241
242
243 void StatusNotifierItem::setState(State state_)
244 {
245     StatusNotifierItemParent::setState(state_);
246
247     emit _statusNotifierItemDBus->NewStatus(metaObject()->enumerator(metaObject()->indexOfEnumerator("State")).valueToKey(state()));
248     emit _statusNotifierItemDBus->NewIcon();
249 }
250
251
252 void StatusNotifierItem::setVisible(bool visible)
253 {
254     if (visible == isVisible())
255         return;
256
257     LegacySystemTray::setVisible(visible);
258
259     if (mode() == StatusNotifier) {
260         if (shouldBeVisible()) {
261             _statusNotifierItemDBus->registerService();
262             registerToDaemon();
263         }
264         else {
265             _statusNotifierItemDBus->unregisterService();
266             _statusNotifierWatcher->deleteLater();
267             _statusNotifierWatcher = 0;
268         }
269     }
270 }
271
272
273 QString StatusNotifierItem::title() const
274 {
275     return QString("Quassel IRC");
276 }
277
278
279 QString StatusNotifierItem::iconName() const
280 {
281     if (state() == Passive)
282         return QString("inactive-quassel");
283     else
284         return QString("quassel");
285 }
286
287
288 QString StatusNotifierItem::attentionIconName() const
289 {
290     if (animationEnabled())
291         return QString("message-quassel");
292     else
293         return QString("quassel");
294 }
295
296
297 QString StatusNotifierItem::toolTipIconName() const
298 {
299     return QString("quassel");
300 }
301
302
303 QString StatusNotifierItem::iconThemePath() const
304 {
305     return _iconThemePath;
306 }
307
308
309 QString StatusNotifierItem::menuObjectPath() const
310 {
311     return _menuObjectPath;
312 }
313
314
315 void StatusNotifierItem::activated(const QPoint &pos)
316 {
317     Q_UNUSED(pos)
318     activate(Trigger);
319 }
320
321
322 bool StatusNotifierItem::eventFilter(QObject *watched, QEvent *event)
323 {
324     if (mode() == StatusNotifier) {
325         //FIXME: ugly ugly workaround to weird QMenu's focus problems
326 #ifdef HAVE_KDE4
327         if (watched == trayMenu() &&
328             (event->type() == QEvent::WindowDeactivate || (event->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton))) {
329             // put at the back of event queue to let the action activate anyways
330             QTimer::singleShot(0, trayMenu(), SLOT(hide()));
331         }
332 #else
333         if (watched == trayMenu() && event->type() == QEvent::HoverLeave) {
334             trayMenu()->hide();
335         }
336 #endif
337     }
338     return StatusNotifierItemParent::eventFilter(watched, event);
339 }
340
341
342 void StatusNotifierItem::showMessage(const QString &title, const QString &message_, SystemTray::MessageIcon icon, int timeout, uint notificationId)
343 {
344     QString message = message_;
345     if (_notificationsClient->isValid()) {
346         if (_notificationsClientSupportsMarkup)
347 #if QT_VERSION < 0x050000
348             message = Qt::escape(message);
349 #else
350             message = message.toHtmlEscaped();
351 #endif
352
353         QStringList actions;
354         if (_notificationsClientSupportsActions)
355             actions << "activate" << "View";
356
357         // we always queue notifications right now
358         QDBusReply<uint> reply = _notificationsClient->Notify(title, 0, "quassel", title, message, actions, QVariantMap(), timeout);
359         if (reply.isValid()) {
360             uint dbusid = reply.value();
361             _notificationsIdMap.insert(dbusid, notificationId);
362             _lastNotificationsDBusId = dbusid;
363         }
364     }
365     else
366         StatusNotifierItemParent::showMessage(title, message, icon, timeout, notificationId);
367 }
368
369
370 void StatusNotifierItem::closeMessage(uint notificationId)
371 {
372     foreach(uint dbusid, _notificationsIdMap.keys()) {
373         if (_notificationsIdMap.value(dbusid) == notificationId) {
374             _notificationsIdMap.remove(dbusid);
375             _notificationsClient->CloseNotification(dbusid);
376         }
377     }
378     _lastNotificationsDBusId = 0;
379 }
380
381
382 void StatusNotifierItem::notificationClosed(uint dbusid, uint reason)
383 {
384     Q_UNUSED(reason)
385     _lastNotificationsDBusId = 0;
386     emit messageClosed(_notificationsIdMap.take(dbusid));
387 }
388
389
390 void StatusNotifierItem::notificationInvoked(uint dbusid, const QString &action)
391 {
392     Q_UNUSED(action)
393     emit messageClicked(_notificationsIdMap.value(dbusid, 0));
394 }
395
396
397 #endif