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