1 /***************************************************************************
2 * Copyright (C) 2005-2019 by the Quassel Project *
3 * devel@quassel-irc.org *
5 * This contains code from KStatusNotifierItem, part of the KDE libs *
6 * Copyright (C) 2009 Marco Martin <notmart@gmail.com> *
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. *
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. *
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 ***************************************************************************/
26 # include "statusnotifieritem.h"
28 # include <QApplication>
33 # include <QMouseEvent>
34 # include <QTextDocument>
39 # include "statusnotifieritemdbus.h"
41 constexpr int kProtocolVersion{0};
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"}};
51 # include "dbusmenuexporter.h"
54 * Specialization to provide access to icon names
56 class QuasselDBusMenuExporter : public DBusMenuExporter
59 QuasselDBusMenuExporter(const QString& dbusObjectPath, QMenu* menu, const QDBusConnection& dbusConnection)
60 : DBusMenuExporter(dbusObjectPath, menu, dbusConnection)
64 QString iconNameForAction(QAction* action) override // TODO Qt 4.7: fixme when we have converted our iconloader
66 QIcon icon(action->icon());
67 return icon.isNull() ? QString() : icon.name();
71 # endif /* HAVE_DBUSMENU */
73 StatusNotifierItem::StatusNotifierItem(QWidget* parent)
74 : StatusNotifierItemParent(parent)
75 , _iconThemeDir{QDir::tempPath() + QLatin1String{"/quassel-sni-XXXXXX"}}
77 static bool registered = []() -> bool {
78 qDBusRegisterMetaType<DBusImageStruct>();
79 qDBusRegisterMetaType<DBusImageVector>();
80 qDBusRegisterMetaType<DBusToolTipStruct>();
85 setMode(Mode::StatusNotifier);
87 connect(this, &StatusNotifierItem::visibilityChanged, this, &StatusNotifierItem::onVisibilityChanged);
88 connect(this, &StatusNotifierItem::modeChanged, this, &StatusNotifierItem::onModeChanged);
89 connect(this, &StatusNotifierItem::stateChanged, this, &StatusNotifierItem::onStateChanged);
91 trayMenu()->installEventFilter(this);
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();
98 qWarning() << "Could not create temporary directory for themed tray icons!";
101 connect(this, &SystemTray::iconsChanged, this, &StatusNotifierItem::refreshIcons);
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);
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);
114 // Client instance for StatusNotifierWatcher
115 _statusNotifierWatcher = new org::kde::StatusNotifierWatcher(kSniWatcherService, kSniWatcherPath, QDBusConnection::sessionBus(), this);
116 connect(_statusNotifierWatcher,
117 &OrgKdeStatusNotifierWatcherInterface::StatusNotifierHostRegistered,
119 &StatusNotifierItem::checkForRegisteredHosts);
120 connect(_statusNotifierWatcher,
121 &OrgKdeStatusNotifierWatcherInterface::StatusNotifierHostUnregistered,
123 &StatusNotifierItem::checkForRegisteredHosts);
125 // Client instance for notifications
126 _notificationsClient = new org::freedesktop::Notifications(kXdgNotificationsService,
127 kXdgNotificationsPath,
128 QDBusConnection::sessionBus(),
130 connect(_notificationsClient, &OrgFreedesktopNotificationsInterface::NotificationClosed, this, &StatusNotifierItem::notificationClosed);
131 connect(_notificationsClient, &OrgFreedesktopNotificationsInterface::ActionInvoked, this, &StatusNotifierItem::notificationInvoked);
133 if (_notificationsClient->isValid()) {
134 QStringList desktopCapabilities = _notificationsClient->GetCapabilities();
135 _notificationsClientSupportsMarkup = desktopCapabilities.contains("body-markup");
136 _notificationsClientSupportsActions = desktopCapabilities.contains("actions");
139 # ifdef HAVE_DBUSMENU
140 new QuasselDBusMenuExporter(menuObjectPath(), trayMenu(), _statusNotifierItemDBus->dbusConnection()); // will be added as menu child
144 void StatusNotifierItem::serviceChange(const QString& name, const QString& oldOwner, const QString& newOwner)
147 if (newOwner.isEmpty()) {
149 setMode(Mode::Legacy);
151 else if (oldOwner.isEmpty()) {
153 setMode(Mode::StatusNotifier);
157 void StatusNotifierItem::registerToWatcher()
159 if (_statusNotifierWatcher->isValid() && _statusNotifierWatcher->property("ProtocolVersion").toInt() == kProtocolVersion) {
160 auto registerMethod = QDBusMessage::createMethodCall(kSniWatcherService,
163 QLatin1String{"RegisterStatusNotifierItem"});
164 registerMethod.setArguments(QVariantList() << _statusNotifierItemDBus->service());
165 _statusNotifierItemDBus->dbusConnection().callWithCallback(registerMethod,
167 SLOT(checkForRegisteredHosts()),
168 SLOT(onDBusError(QDBusError)));
171 setMode(Mode::Legacy);
175 void StatusNotifierItem::checkForRegisteredHosts()
177 if (!_statusNotifierWatcher || !_statusNotifierWatcher->property("IsStatusNotifierHostRegistered").toBool()) {
178 setMode(Mode::Legacy);
181 setMode(Mode::StatusNotifier);
185 void StatusNotifierItem::onDBusError(const QDBusError& error)
187 qWarning() << "StatusNotifierItem encountered a D-Bus error:" << error;
188 setMode(Mode::Legacy);
191 void StatusNotifierItem::refreshIcons()
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;
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;
220 if (_statusNotifierItemDBus) {
221 emit _statusNotifierItemDBus->NewIcon();
222 emit _statusNotifierItemDBus->NewAttentionIcon();
226 bool StatusNotifierItem::isSystemTrayAvailable() const
228 if (mode() == Mode::StatusNotifier) {
229 return true; // else it should be set to legacy on registration
232 return StatusNotifierItemParent::isSystemTrayAvailable();
235 void StatusNotifierItem::onModeChanged(Mode mode)
237 if (mode == Mode::StatusNotifier) {
238 _statusNotifierItemDBus->registerTrayIcon();
242 _statusNotifierItemDBus->unregisterTrayIcon();
246 void StatusNotifierItem::onStateChanged(State state)
248 if (mode() == Mode::StatusNotifier) {
249 emit _statusNotifierItemDBus->NewStatus(metaObject()->enumerator(metaObject()->indexOfEnumerator("State")).valueToKey(state));
253 void StatusNotifierItem::onVisibilityChanged(bool isVisible)
255 if (mode() == Mode::StatusNotifier) {
257 _statusNotifierItemDBus->registerTrayIcon();
261 _statusNotifierItemDBus->unregisterTrayIcon();
266 QString StatusNotifierItem::title() const
268 return QString("Quassel IRC");
271 QString StatusNotifierItem::iconName() const
273 return currentIconName();
276 QString StatusNotifierItem::attentionIconName() const
278 return currentAttentionIconName();
281 QString StatusNotifierItem::toolTipIconName() const
286 QString StatusNotifierItem::iconThemePath() const
288 return _iconThemePath;
291 QString StatusNotifierItem::menuObjectPath() const
293 return kMenuObjectPath;
296 void StatusNotifierItem::activated(const QPoint& pos)
302 bool StatusNotifierItem::eventFilter(QObject* watched, QEvent* event)
304 if (mode() == StatusNotifier) {
305 if (watched == trayMenu() && event->type() == QEvent::HoverLeave) {
309 return StatusNotifierItemParent::eventFilter(watched, event);
312 void StatusNotifierItem::showMessage(const QString& title, const QString& message_, SystemTray::MessageIcon icon, int timeout, uint notificationId)
314 QString message = message_;
315 if (_notificationsClient->isValid()) {
316 if (_notificationsClientSupportsMarkup) {
317 message = message.toHtmlEscaped();
321 if (_notificationsClientSupportsActions)
322 actions << "activate"
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;
334 StatusNotifierItemParent::showMessage(title, message, icon, timeout, notificationId);
337 void StatusNotifierItem::closeMessage(uint notificationId)
339 for (auto&& dbusid : _notificationsIdMap.keys()) {
340 if (_notificationsIdMap.value(dbusid) == notificationId) {
341 _notificationsIdMap.remove(dbusid);
342 _notificationsClient->CloseNotification(dbusid);
345 _lastNotificationsDBusId = 0;
347 StatusNotifierItemParent::closeMessage(notificationId);
350 void StatusNotifierItem::notificationClosed(uint dbusid, uint reason)
353 _lastNotificationsDBusId = 0;
354 emit messageClosed(_notificationsIdMap.take(dbusid));
357 void StatusNotifierItem::notificationInvoked(uint dbusid, const QString& action)
360 emit messageClicked(_notificationsIdMap.value(dbusid, 0));