cmake: avoid de-duplication of user's CXXFLAGS
[quassel.git] / src / qtui / statusnotifieritem.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2022 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     QString iconNameForAction(QAction* action) override  // 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     , _iconThemeDir{QDir::tempPath() + QLatin1String{"/quassel-sni-XXXXXX"}}
76 {
77     static bool registered = []() -> bool {
78         qDBusRegisterMetaType<DBusImageStruct>();
79         qDBusRegisterMetaType<DBusImageVector>();
80         qDBusRegisterMetaType<DBusToolTipStruct>();
81         return true;
82     }();
83     Q_UNUSED(registered)
84
85     setMode(Mode::StatusNotifier);
86
87     connect(this, &StatusNotifierItem::visibilityChanged, this, &StatusNotifierItem::onVisibilityChanged);
88     connect(this, &StatusNotifierItem::modeChanged, this, &StatusNotifierItem::onModeChanged);
89     connect(this, &StatusNotifierItem::stateChanged, this, &StatusNotifierItem::onStateChanged);
90
91     trayMenu()->installEventFilter(this);
92
93     // Create a temporary directory that holds copies of the tray icons. That way, visualizers can find our icons.
94     if (_iconThemeDir.isValid()) {
95         _iconThemePath = _iconThemeDir.path();
96     }
97     else {
98         qWarning() << "Could not create temporary directory for themed tray icons!";
99     }
100
101     connect(this, &SystemTray::iconsChanged, this, &StatusNotifierItem::refreshIcons);
102     refreshIcons();
103
104     // Our own SNI service
105     _statusNotifierItemDBus = new StatusNotifierItemDBus(this);
106     connect(this, &StatusNotifierItem::currentIconNameChanged, _statusNotifierItemDBus, &StatusNotifierItemDBus::NewIcon);
107     connect(this, &StatusNotifierItem::currentIconNameChanged, _statusNotifierItemDBus, &StatusNotifierItemDBus::NewAttentionIcon);
108     connect(this, &StatusNotifierItem::toolTipChanged, _statusNotifierItemDBus, &StatusNotifierItemDBus::NewToolTip);
109
110     // Service watcher to keep track of the StatusNotifierWatcher service
111     _serviceWatcher = new QDBusServiceWatcher(kSniWatcherService, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
112     connect(_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &StatusNotifierItem::serviceChange);
113
114     // Client instance for StatusNotifierWatcher
115     _statusNotifierWatcher = new org::kde::StatusNotifierWatcher(kSniWatcherService, kSniWatcherPath, QDBusConnection::sessionBus(), this);
116     connect(_statusNotifierWatcher,
117             &OrgKdeStatusNotifierWatcherInterface::StatusNotifierHostRegistered,
118             this,
119             &StatusNotifierItem::checkForRegisteredHosts);
120     connect(_statusNotifierWatcher,
121             &OrgKdeStatusNotifierWatcherInterface::StatusNotifierHostUnregistered,
122             this,
123             &StatusNotifierItem::checkForRegisteredHosts);
124
125     // Client instance for notifications
126     _notificationsClient = new org::freedesktop::Notifications(kXdgNotificationsService,
127                                                                kXdgNotificationsPath,
128                                                                QDBusConnection::sessionBus(),
129                                                                this);
130     connect(_notificationsClient, &OrgFreedesktopNotificationsInterface::NotificationClosed, this, &StatusNotifierItem::notificationClosed);
131     connect(_notificationsClient, &OrgFreedesktopNotificationsInterface::ActionInvoked, this, &StatusNotifierItem::notificationInvoked);
132
133     if (_notificationsClient->isValid()) {
134         QStringList desktopCapabilities = _notificationsClient->GetCapabilities();
135         _notificationsClientSupportsMarkup = desktopCapabilities.contains("body-markup");
136         _notificationsClientSupportsActions = desktopCapabilities.contains("actions");
137     }
138
139 #    ifdef HAVE_DBUSMENU
140     new QuasselDBusMenuExporter(menuObjectPath(), trayMenu(), _statusNotifierItemDBus->dbusConnection());  // will be added as menu child
141 #    endif
142 }
143
144 void StatusNotifierItem::serviceChange(const QString& name, const QString& oldOwner, const QString& newOwner)
145 {
146     Q_UNUSED(name);
147     if (newOwner.isEmpty()) {
148         // unregistered
149         setMode(Mode::Legacy);
150     }
151     else if (oldOwner.isEmpty()) {
152         // registered
153         setMode(Mode::StatusNotifier);
154     }
155 }
156
157 void StatusNotifierItem::registerToWatcher()
158 {
159     if (_statusNotifierWatcher->isValid() && _statusNotifierWatcher->property("ProtocolVersion").toInt() == kProtocolVersion) {
160         auto registerMethod = QDBusMessage::createMethodCall(kSniWatcherService,
161                                                              kSniWatcherPath,
162                                                              kSniWatcherService,
163                                                              QLatin1String{"RegisterStatusNotifierItem"});
164         registerMethod.setArguments(QVariantList() << _statusNotifierItemDBus->service());
165         _statusNotifierItemDBus->dbusConnection().callWithCallback(registerMethod,
166                                                                    this,
167                                                                    SLOT(checkForRegisteredHosts()),
168                                                                    SLOT(onDBusError(QDBusError)));
169     }
170     else {
171         setMode(Mode::Legacy);
172     }
173 }
174
175 void StatusNotifierItem::checkForRegisteredHosts()
176 {
177     if (!_statusNotifierWatcher || !_statusNotifierWatcher->property("IsStatusNotifierHostRegistered").toBool()) {
178         setMode(Mode::Legacy);
179     }
180     else {
181         setMode(Mode::StatusNotifier);
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 void StatusNotifierItem::refreshIcons()
192 {
193     if (!_iconThemePath.isEmpty()) {
194         QDir baseDir{_iconThemePath + "/hicolor"};
195         baseDir.removeRecursively();
196         for (auto&& trayState : {State::Active, State::Passive, State::NeedsAttention}) {
197             auto iconName = SystemTray::iconName(trayState);
198             QIcon icon = icon::get(iconName);
199             if (!icon.isNull()) {
200                 for (auto&& size : icon.availableSizes()) {
201                     auto pixDir = QString{"%1/%2x%3/status"}.arg(baseDir.absolutePath()).arg(size.width()).arg(size.height());
202                     QDir{}.mkpath(pixDir);
203                     if (!icon.pixmap(size).save(pixDir + "/" + iconName + ".png")) {
204                         qWarning() << "Could not save tray icon" << iconName << "for size" << size;
205                     }
206                 }
207             }
208             else {
209                 // No theme icon found; use fallback from resources
210                 auto iconDir = QString{"%1/24x24/status"}.arg(baseDir.absolutePath());
211                 QDir{}.mkpath(iconDir);
212                 if (!QFile::copy(QString{":/icons/hicolor/24x24/status/%1.svg"}.arg(iconName), QString{"%1/%2.svg"}.arg(iconDir, iconName))) {
213                     qWarning() << "Could not access fallback tray icon" << iconName;
214                     continue;
215                 }
216             }
217         }
218     }
219
220     if (_statusNotifierItemDBus) {
221         emit _statusNotifierItemDBus->NewIcon();
222         emit _statusNotifierItemDBus->NewAttentionIcon();
223     }
224 }
225
226 bool StatusNotifierItem::isSystemTrayAvailable() const
227 {
228     if (mode() == Mode::StatusNotifier) {
229         return true;  // else it should be set to legacy on registration
230     }
231
232     return StatusNotifierItemParent::isSystemTrayAvailable();
233 }
234
235 void StatusNotifierItem::onModeChanged(Mode mode)
236 {
237     if (mode == Mode::StatusNotifier) {
238         _statusNotifierItemDBus->registerTrayIcon();
239         registerToWatcher();
240     }
241     else {
242         _statusNotifierItemDBus->unregisterTrayIcon();
243     }
244 }
245
246 void StatusNotifierItem::onStateChanged(State state)
247 {
248     if (mode() == Mode::StatusNotifier) {
249         emit _statusNotifierItemDBus->NewStatus(metaObject()->enumerator(metaObject()->indexOfEnumerator("State")).valueToKey(state));
250     }
251 }
252
253 void StatusNotifierItem::onVisibilityChanged(bool isVisible)
254 {
255     if (mode() == Mode::StatusNotifier) {
256         if (isVisible) {
257             _statusNotifierItemDBus->registerTrayIcon();
258             registerToWatcher();
259         }
260         else {
261             _statusNotifierItemDBus->unregisterTrayIcon();
262         }
263     }
264 }
265
266 QString StatusNotifierItem::title() const
267 {
268     return QString("Quassel IRC");
269 }
270
271 QString StatusNotifierItem::iconName() const
272 {
273     return currentIconName();
274 }
275
276 QString StatusNotifierItem::attentionIconName() const
277 {
278     return currentAttentionIconName();
279 }
280
281 QString StatusNotifierItem::toolTipIconName() const
282 {
283     return "quassel";
284 }
285
286 QString StatusNotifierItem::iconThemePath() const
287 {
288     return _iconThemePath;
289 }
290
291 QString StatusNotifierItem::menuObjectPath() const
292 {
293     return kMenuObjectPath;
294 }
295
296 void StatusNotifierItem::activated(const QPoint& pos)
297 {
298     Q_UNUSED(pos)
299     activate(Trigger);
300 }
301
302 bool StatusNotifierItem::eventFilter(QObject* watched, QEvent* event)
303 {
304     if (mode() == StatusNotifier) {
305         if (watched == trayMenu() && event->type() == QEvent::HoverLeave) {
306             trayMenu()->hide();
307         }
308     }
309     return StatusNotifierItemParent::eventFilter(watched, event);
310 }
311
312 void StatusNotifierItem::showMessage(const QString& title, const QString& message_, SystemTray::MessageIcon icon, int timeout, uint notificationId)
313 {
314     QString message = message_;
315     if (_notificationsClient->isValid()) {
316         if (_notificationsClientSupportsMarkup) {
317             message = message.toHtmlEscaped();
318         }
319
320         QStringList actions;
321         if (_notificationsClientSupportsActions)
322             actions << "activate"
323                     << "View";
324
325         // we always queue notifications right now
326         QDBusReply<uint> reply = _notificationsClient->Notify(title, 0, "quassel", title, message, actions, QVariantMap(), timeout);
327         if (reply.isValid()) {
328             uint dbusid = reply.value();
329             _notificationsIdMap.insert(dbusid, notificationId);
330             _lastNotificationsDBusId = dbusid;
331         }
332     }
333     else
334         StatusNotifierItemParent::showMessage(title, message, icon, timeout, notificationId);
335 }
336
337 void StatusNotifierItem::closeMessage(uint notificationId)
338 {
339     for (auto&& dbusid : _notificationsIdMap.keys()) {
340         if (_notificationsIdMap.value(dbusid) == notificationId) {
341             _notificationsIdMap.remove(dbusid);
342             _notificationsClient->CloseNotification(dbusid);
343         }
344     }
345     _lastNotificationsDBusId = 0;
346
347     StatusNotifierItemParent::closeMessage(notificationId);
348 }
349
350 void StatusNotifierItem::notificationClosed(uint dbusid, uint reason)
351 {
352     Q_UNUSED(reason)
353     _lastNotificationsDBusId = 0;
354     emit messageClosed(_notificationsIdMap.take(dbusid));
355 }
356
357 void StatusNotifierItem::notificationInvoked(uint dbusid, const QString& action)
358 {
359     Q_UNUSED(action)
360     emit messageClicked(_notificationsIdMap.value(dbusid, 0));
361 }
362
363 #endif