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