0965eeff7913da7fe987b0c0f1ec085bfa338155
[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             QIcon icon = QIcon::fromTheme(SystemTray::iconName(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 SystemTray::iconName(State::Passive);
283     }
284     else {
285         return SystemTray::iconName(State::Active);
286     }
287 }
288
289
290 QString StatusNotifierItem::attentionIconName() const
291 {
292     if (animationEnabled()) {
293         return SystemTray::iconName(State::NeedsAttention);
294     }
295     else {
296         return SystemTray::iconName(State::Active);
297     }
298 }
299
300
301 QString StatusNotifierItem::toolTipIconName() const
302 {
303     return "quassel";
304 }
305
306
307 QString StatusNotifierItem::iconThemePath() const
308 {
309     return _iconThemePath;
310 }
311
312
313 QString StatusNotifierItem::menuObjectPath() const
314 {
315     return _menuObjectPath;
316 }
317
318
319 void StatusNotifierItem::activated(const QPoint &pos)
320 {
321     Q_UNUSED(pos)
322     activate(Trigger);
323 }
324
325
326 bool StatusNotifierItem::eventFilter(QObject *watched, QEvent *event)
327 {
328     if (mode() == StatusNotifier) {
329         //FIXME: ugly ugly workaround to weird QMenu's focus problems
330 #ifdef HAVE_KDE4
331         if (watched == trayMenu() &&
332             (event->type() == QEvent::WindowDeactivate || (event->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton))) {
333             // put at the back of event queue to let the action activate anyways
334             QTimer::singleShot(0, trayMenu(), SLOT(hide()));
335         }
336 #else
337         if (watched == trayMenu() && event->type() == QEvent::HoverLeave) {
338             trayMenu()->hide();
339         }
340 #endif
341     }
342     return StatusNotifierItemParent::eventFilter(watched, event);
343 }
344
345
346 void StatusNotifierItem::showMessage(const QString &title, const QString &message_, SystemTray::MessageIcon icon, int timeout, uint notificationId)
347 {
348     QString message = message_;
349     if (_notificationsClient->isValid()) {
350         if (_notificationsClientSupportsMarkup)
351 #if QT_VERSION < 0x050000
352             message = Qt::escape(message);
353 #else
354             message = message.toHtmlEscaped();
355 #endif
356
357         QStringList actions;
358         if (_notificationsClientSupportsActions)
359             actions << "activate" << "View";
360
361         // we always queue notifications right now
362         QDBusReply<uint> reply = _notificationsClient->Notify(title, 0, "quassel", title, message, actions, QVariantMap(), timeout);
363         if (reply.isValid()) {
364             uint dbusid = reply.value();
365             _notificationsIdMap.insert(dbusid, notificationId);
366             _lastNotificationsDBusId = dbusid;
367         }
368     }
369     else
370         StatusNotifierItemParent::showMessage(title, message, icon, timeout, notificationId);
371 }
372
373
374 void StatusNotifierItem::closeMessage(uint notificationId)
375 {
376     foreach(uint dbusid, _notificationsIdMap.keys()) {
377         if (_notificationsIdMap.value(dbusid) == notificationId) {
378             _notificationsIdMap.remove(dbusid);
379             _notificationsClient->CloseNotification(dbusid);
380         }
381     }
382     _lastNotificationsDBusId = 0;
383 }
384
385
386 void StatusNotifierItem::notificationClosed(uint dbusid, uint reason)
387 {
388     Q_UNUSED(reason)
389     _lastNotificationsDBusId = 0;
390     emit messageClosed(_notificationsIdMap.take(dbusid));
391 }
392
393
394 void StatusNotifierItem::notificationInvoked(uint dbusid, const QString &action)
395 {
396     Q_UNUSED(action)
397     emit messageClicked(_notificationsIdMap.value(dbusid, 0));
398 }
399
400
401 #endif