X-Git-Url: https://git.quassel-irc.org/?p=quassel.git;a=blobdiff_plain;f=src%2Fqtui%2Fstatusnotifieritem.cpp;h=a0beb169733ff6dcb78e6cb4c7781a6f73ca6ca1;hp=08197f8adbe933aa2d6a394daa36a6def3d94978;hb=518cd2bc478ab3675a60ec46d3ef183cace0cae7;hpb=5d9af8ed007d38faf3995ea18174249121f246fe diff --git a/src/qtui/statusnotifieritem.cpp b/src/qtui/statusnotifieritem.cpp index 08197f8a..a0beb169 100644 --- a/src/qtui/statusnotifieritem.cpp +++ b/src/qtui/statusnotifieritem.cpp @@ -1,14 +1,14 @@ /*************************************************************************** - * Copyright (C) 2005-2010 by the Quassel Project * + * Copyright (C) 2005-2020 by the Quassel Project * * devel@quassel-irc.org * * * * This contains code from KStatusNotifierItem, part of the KDE libs * * Copyright (C) 2009 Marco Martin * * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) version 3. * + * This file is free software; you can redistribute it and/or modify * + * it under the terms of the GNU Library General Public License (LGPL) * + * as published by the Free Software Foundation; either version 2 of the * + * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * @@ -18,294 +18,346 @@ * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #ifdef HAVE_DBUS -#include -#include -#include -#include +# include "statusnotifieritem.h" -#include "quassel.h" -#include "statusnotifieritem.h" -#include "statusnotifieritemdbus.h" +# include +# include +# include +# include +# include +# include +# include -const int StatusNotifierItem::_protocolVersion = 0; +# include "icon.h" +# include "qtui.h" +# include "quassel.h" +# include "statusnotifieritemdbus.h" -#ifdef HAVE_DBUSMENU -# include "dbusmenuexporter.h" +constexpr int kProtocolVersion{0}; + +const QString kSniWatcherService{QLatin1String{"org.kde.StatusNotifierWatcher"}}; +const QString kSniWatcherPath{QLatin1String{"/StatusNotifierWatcher"}}; +const QString kSniPath{QLatin1String{"/StatusNotifierItem"}}; +const QString kXdgNotificationsService{QLatin1String{"org.freedesktop.Notifications"}}; +const QString kXdgNotificationsPath{QLatin1String{"/org/freedesktop/Notifications"}}; +const QString kMenuObjectPath{QLatin1String{"/MenuBar"}}; + +# ifdef HAVE_DBUSMENU +# include "dbusmenuexporter.h" /** * Specialization to provide access to icon names */ -class QuasselDBusMenuExporter : public DBusMenuExporter { +class QuasselDBusMenuExporter : public DBusMenuExporter +{ public: - QuasselDBusMenuExporter(const QString &dbusObjectPath, QMenu *menu, const QDBusConnection &dbusConnection) - : DBusMenuExporter(dbusObjectPath, menu, dbusConnection) - {} + QuasselDBusMenuExporter(const QString& dbusObjectPath, QMenu* menu, const QDBusConnection& dbusConnection) + : DBusMenuExporter(dbusObjectPath, menu, dbusConnection) + {} protected: - virtual QString iconNameForAction(QAction *action) { // TODO Qt 4.7: fixme when we have converted our iconloader - Icon icon(action->icon()); -#if QT_VERSION >= 0x040701 - // QIcon::name() is in the 4.7 git branch, but it is not in 4.7 TP. - // If you get a build error here, you need to update your pre-release - // of Qt 4.7. - return icon.isNull() ? QString() : icon.name(); -#else - return QString(); -#endif - } + QString iconNameForAction(QAction* action) override // TODO Qt 4.7: fixme when we have converted our iconloader + { + QIcon icon(action->icon()); + return icon.isNull() ? QString() : icon.name(); + } }; -#endif /* HAVE_DBUSMENU */ +# endif /* HAVE_DBUSMENU */ -StatusNotifierItem::StatusNotifierItem(QWidget *parent) - : StatusNotifierItemParent(parent), - _statusNotifierItemDBus(0), - _statusNotifierWatcher(0), - _notificationsClient(0), - _notificationsClientSupportsMarkup(true), - _lastNotificationsDBusId(0) +StatusNotifierItem::StatusNotifierItem(QWidget* parent) + : StatusNotifierItemParent(parent) + , _iconThemeDir{QDir::tempPath() + QLatin1String{"/quassel-sni-XXXXXX"}} { + static bool registered = []() -> bool { + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + return true; + }(); + Q_UNUSED(registered) -} - -StatusNotifierItem::~StatusNotifierItem() { - delete _statusNotifierWatcher; -} + setMode(Mode::StatusNotifier); -void StatusNotifierItem::init() { - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); + connect(this, &StatusNotifierItem::visibilityChanged, this, &StatusNotifierItem::onVisibilityChanged); + connect(this, &StatusNotifierItem::modeChanged, this, &StatusNotifierItem::onModeChanged); + connect(this, &StatusNotifierItem::stateChanged, this, &StatusNotifierItem::onStateChanged); - _statusNotifierItemDBus = new StatusNotifierItemDBus(this); + trayMenu()->installEventFilter(this); - connect(this, SIGNAL(toolTipChanged(QString,QString)), _statusNotifierItemDBus, SIGNAL(NewToolTip())); - connect(this, SIGNAL(animationEnabledChanged(bool)), _statusNotifierItemDBus, SIGNAL(NewAttentionIcon())); - - connect(QDBusConnection::sessionBus().interface(), SIGNAL(serviceOwnerChanged(QString,QString,QString)), - SLOT(serviceChange(QString,QString,QString))); - - setMode(StatusNotifier); - - _notificationsClient = new org::freedesktop::Notifications("org.freedesktop.Notifications", "/org/freedesktop/Notifications", - QDBusConnection::sessionBus(), this); - - connect(_notificationsClient, SIGNAL(NotificationClosed(uint,uint)), SLOT(notificationClosed(uint,uint))); - connect(_notificationsClient, SIGNAL(ActionInvoked(uint,QString)), SLOT(notificationInvoked(uint,QString))); - - if(_notificationsClient->isValid()) { - QStringList desktopCapabilities = _notificationsClient->GetCapabilities(); - _notificationsClientSupportsMarkup = desktopCapabilities.contains("body-markup"); - } - - StatusNotifierItemParent::init(); - trayMenu()->installEventFilter(this); + // Create a temporary directory that holds copies of the tray icons. That way, visualizers can find our icons. + if (_iconThemeDir.isValid()) { + _iconThemePath = _iconThemeDir.path(); + } + else { + qWarning() << "Could not create temporary directory for themed tray icons!"; + } - // use the appdata icon folder for now - _iconThemePath = Quassel::findDataFilePath("icons"); + connect(this, &SystemTray::iconsChanged, this, &StatusNotifierItem::refreshIcons); + refreshIcons(); + + // Our own SNI service + _statusNotifierItemDBus = new StatusNotifierItemDBus(this); + connect(this, &StatusNotifierItem::currentIconNameChanged, _statusNotifierItemDBus, &StatusNotifierItemDBus::NewIcon); + connect(this, &StatusNotifierItem::currentIconNameChanged, _statusNotifierItemDBus, &StatusNotifierItemDBus::NewAttentionIcon); + connect(this, &StatusNotifierItem::toolTipChanged, _statusNotifierItemDBus, &StatusNotifierItemDBus::NewToolTip); + + // Service watcher to keep track of the StatusNotifierWatcher service + _serviceWatcher = new QDBusServiceWatcher(kSniWatcherService, QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this); + connect(_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &StatusNotifierItem::serviceChange); + + // Client instance for StatusNotifierWatcher + _statusNotifierWatcher = new org::kde::StatusNotifierWatcher(kSniWatcherService, kSniWatcherPath, QDBusConnection::sessionBus(), this); + connect(_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierHostRegistered, + this, + &StatusNotifierItem::checkForRegisteredHosts); + connect(_statusNotifierWatcher, + &OrgKdeStatusNotifierWatcherInterface::StatusNotifierHostUnregistered, + this, + &StatusNotifierItem::checkForRegisteredHosts); + + // Client instance for notifications + _notificationsClient = new org::freedesktop::Notifications(kXdgNotificationsService, + kXdgNotificationsPath, + QDBusConnection::sessionBus(), + this); + connect(_notificationsClient, &OrgFreedesktopNotificationsInterface::NotificationClosed, this, &StatusNotifierItem::notificationClosed); + connect(_notificationsClient, &OrgFreedesktopNotificationsInterface::ActionInvoked, this, &StatusNotifierItem::notificationInvoked); + + if (_notificationsClient->isValid()) { + QStringList desktopCapabilities = _notificationsClient->GetCapabilities(); + _notificationsClientSupportsMarkup = desktopCapabilities.contains("body-markup"); + _notificationsClientSupportsActions = desktopCapabilities.contains("actions"); + } -#ifdef HAVE_DBUSMENU - _menuObjectPath = "/MenuBar"; - new QuasselDBusMenuExporter(menuObjectPath(), trayMenu(), _statusNotifierItemDBus->dbusConnection()); // will be added as menu child -#endif +# ifdef HAVE_DBUSMENU + new QuasselDBusMenuExporter(menuObjectPath(), trayMenu(), _statusNotifierItemDBus->dbusConnection()); // will be added as menu child +# endif } -void StatusNotifierItem::registerToDaemon() { - if(!_statusNotifierWatcher) { - QString interface("org.kde.StatusNotifierWatcher"); - _statusNotifierWatcher = new org::kde::StatusNotifierWatcher(interface, "/StatusNotifierWatcher", QDBusConnection::sessionBus()); - } - if(_statusNotifierWatcher->isValid() - && _statusNotifierWatcher->property("ProtocolVersion").toInt() == _protocolVersion) { - - _statusNotifierWatcher->RegisterStatusNotifierItem(_statusNotifierItemDBus->service()); - - } else { - //qDebug() << "StatusNotifierWatcher not reachable!"; - setMode(Legacy); - } +void StatusNotifierItem::serviceChange(const QString& name, const QString& oldOwner, const QString& newOwner) +{ + Q_UNUSED(name); + if (newOwner.isEmpty()) { + // unregistered + setMode(Mode::Legacy); + } + else if (oldOwner.isEmpty()) { + // registered + setMode(Mode::StatusNotifier); + } } -// FIXME remove deprecated slot with Qt 4.6 -void StatusNotifierItem::serviceChange(const QString& name, const QString& oldOwner, const QString& newOwner) { - bool legacy = false; - if(name == "org.kde.StatusNotifierWatcher") { - if(newOwner.isEmpty()) { - //unregistered - //qDebug() << "Connection to the StatusNotifierWatcher lost"; - legacy = true; - } else if(oldOwner.isEmpty()) { - //registered - legacy = false; +void StatusNotifierItem::registerToWatcher() +{ + if (_statusNotifierWatcher->isValid() && _statusNotifierWatcher->property("ProtocolVersion").toInt() == kProtocolVersion) { + auto registerMethod = QDBusMessage::createMethodCall(kSniWatcherService, + kSniWatcherPath, + kSniWatcherService, + QLatin1String{"RegisterStatusNotifierItem"}); + registerMethod.setArguments(QVariantList() << _statusNotifierItemDBus->service()); + _statusNotifierItemDBus->dbusConnection().callWithCallback(registerMethod, + this, + SLOT(checkForRegisteredHosts()), + SLOT(onDBusError(QDBusError))); } - } else if(name.startsWith(QLatin1String("org.kde.StatusNotifierHost-"))) { - if(newOwner.isEmpty() && (!_statusNotifierWatcher || - !_statusNotifierWatcher->property("IsStatusNotifierHostRegistered").toBool())) { - //qDebug() << "Connection to the last StatusNotifierHost lost"; - legacy = true; - } else if(oldOwner.isEmpty()) { - //qDebug() << "New StatusNotifierHost"; - legacy = false; + else { + setMode(Mode::Legacy); } - } else { - return; - } - - // qDebug() << "Service " << name << "status change, old owner:" << oldOwner << "new:" << newOwner; - - if(legacy == (mode() == Legacy)) { - return; - } - - if(legacy) { - //unregistered - setMode(Legacy); - } else { - //registered - setMode(StatusNotifier); - } } -bool StatusNotifierItem::isSystemTrayAvailable() const { - if(mode() == StatusNotifier) - return true; // else it should be set to legacy on registration +void StatusNotifierItem::checkForRegisteredHosts() +{ + if (!_statusNotifierWatcher || !_statusNotifierWatcher->property("IsStatusNotifierHostRegistered").toBool()) { + setMode(Mode::Legacy); + } + else { + setMode(Mode::StatusNotifier); + } +} - return StatusNotifierItemParent::isSystemTrayAvailable(); +void StatusNotifierItem::onDBusError(const QDBusError& error) +{ + qWarning() << "StatusNotifierItem encountered a D-Bus error:" << error; + setMode(Mode::Legacy); } -bool StatusNotifierItem::isVisible() const { - if(mode() == StatusNotifier) - return shouldBeVisible(); // we don't have a way to check, so we need to trust everything went right +void StatusNotifierItem::refreshIcons() +{ + if (!_iconThemePath.isEmpty()) { + QDir baseDir{_iconThemePath + "/hicolor"}; + baseDir.removeRecursively(); + for (auto&& trayState : {State::Active, State::Passive, State::NeedsAttention}) { + auto iconName = SystemTray::iconName(trayState); + QIcon icon = icon::get(iconName); + if (!icon.isNull()) { + for (auto&& size : icon.availableSizes()) { + auto pixDir = QString{"%1/%2x%3/status"}.arg(baseDir.absolutePath()).arg(size.width()).arg(size.height()); + QDir{}.mkpath(pixDir); + if (!icon.pixmap(size).save(pixDir + "/" + iconName + ".png")) { + qWarning() << "Could not save tray icon" << iconName << "for size" << size; + } + } + } + else { + // No theme icon found; use fallback from resources + auto iconDir = QString{"%1/24x24/status"}.arg(baseDir.absolutePath()); + QDir{}.mkpath(iconDir); + if (!QFile::copy(QString{":/icons/hicolor/24x24/status/%1.svg"}.arg(iconName), QString{"%1/%2.svg"}.arg(iconDir, iconName))) { + qWarning() << "Could not access fallback tray icon" << iconName; + continue; + } + } + } + } - return StatusNotifierItemParent::isVisible(); + if (_statusNotifierItemDBus) { + emit _statusNotifierItemDBus->NewIcon(); + emit _statusNotifierItemDBus->NewAttentionIcon(); + } } -void StatusNotifierItem::setMode(Mode mode_) { - StatusNotifierItemParent::setMode(mode_); +bool StatusNotifierItem::isSystemTrayAvailable() const +{ + if (mode() == Mode::StatusNotifier) { + return true; // else it should be set to legacy on registration + } - if(mode() == StatusNotifier) { - registerToDaemon(); - } + return StatusNotifierItemParent::isSystemTrayAvailable(); } -void StatusNotifierItem::setState(State state_) { - StatusNotifierItemParent::setState(state_); +void StatusNotifierItem::onModeChanged(Mode mode) +{ + if (mode == Mode::StatusNotifier) { + _statusNotifierItemDBus->registerTrayIcon(); + registerToWatcher(); + } + else { + _statusNotifierItemDBus->unregisterTrayIcon(); + } +} - emit _statusNotifierItemDBus->NewStatus(metaObject()->enumerator(metaObject()->indexOfEnumerator("State")).valueToKey(state())); - emit _statusNotifierItemDBus->NewIcon(); +void StatusNotifierItem::onStateChanged(State state) +{ + if (mode() == Mode::StatusNotifier) { + emit _statusNotifierItemDBus->NewStatus(metaObject()->enumerator(metaObject()->indexOfEnumerator("State")).valueToKey(state)); + } } -void StatusNotifierItem::setVisible(bool visible) { - LegacySystemTray::setVisible(visible); - - if(mode() == StatusNotifier) { - if(shouldBeVisible()) { - _statusNotifierItemDBus->registerService(); - registerToDaemon(); - } else { - _statusNotifierItemDBus->unregisterService(); - _statusNotifierWatcher->deleteLater(); - _statusNotifierWatcher = 0; +void StatusNotifierItem::onVisibilityChanged(bool isVisible) +{ + if (mode() == Mode::StatusNotifier) { + if (isVisible) { + _statusNotifierItemDBus->registerTrayIcon(); + registerToWatcher(); + } + else { + _statusNotifierItemDBus->unregisterTrayIcon(); + } } - } } -QString StatusNotifierItem::title() const { - return QString("Quassel IRC"); +QString StatusNotifierItem::title() const +{ + return QString("Quassel IRC"); } -QString StatusNotifierItem::iconName() const { - if(state() == Passive) - return QString("quassel_inactive"); - else - return QString("quassel"); +QString StatusNotifierItem::iconName() const +{ + return currentIconName(); } -QString StatusNotifierItem::attentionIconName() const { - if(animationEnabled()) - return QString("quassel_message"); - else - return QString("quassel"); +QString StatusNotifierItem::attentionIconName() const +{ + return currentAttentionIconName(); } -QString StatusNotifierItem::toolTipIconName() const { - return QString("quassel"); +QString StatusNotifierItem::toolTipIconName() const +{ + return "quassel"; } -QString StatusNotifierItem::iconThemePath() const { - return _iconThemePath; +QString StatusNotifierItem::iconThemePath() const +{ + return _iconThemePath; } -QString StatusNotifierItem::menuObjectPath() const { - return _menuObjectPath; +QString StatusNotifierItem::menuObjectPath() const +{ + return kMenuObjectPath; } -void StatusNotifierItem::activated(const QPoint &pos) { - Q_UNUSED(pos) - activate(Trigger); +void StatusNotifierItem::activated(const QPoint& pos) +{ + Q_UNUSED(pos) + activate(Trigger); } -bool StatusNotifierItem::eventFilter(QObject *watched, QEvent *event) { - if(mode() == StatusNotifier) { - //FIXME: ugly ugly workaround to weird QMenu's focus problems -#ifdef HAVE_KDE - if(watched == trayMenu() && - (event->type() == QEvent::WindowDeactivate || (event->type() == QEvent::MouseButtonRelease && static_cast(event)->button() == Qt::LeftButton))) { - // put at the back of event queue to let the action activate anyways - QTimer::singleShot(0, trayMenu(), SLOT(hide())); - } -#else - if(watched == trayMenu() && event->type() == QEvent::HoverLeave) { - trayMenu()->hide(); +bool StatusNotifierItem::eventFilter(QObject* watched, QEvent* event) +{ + if (mode() == StatusNotifier) { + if (watched == trayMenu() && event->type() == QEvent::HoverLeave) { + trayMenu()->hide(); + } } -#endif - } - return StatusNotifierItemParent::eventFilter(watched, event); + return StatusNotifierItemParent::eventFilter(watched, event); } -void StatusNotifierItem::showMessage(const QString &title, const QString &message_, SystemTray::MessageIcon icon, int timeout, uint notificationId) { - QString message = message_; - if(_notificationsClient->isValid()) { - if(_notificationsClientSupportsMarkup) - message = Qt::escape(message); - - QStringList actions = QStringList() << "activate" << "View"; - - // we always queue notifications right now - QDBusReply reply = _notificationsClient->Notify(title, 0, "quassel", title, message, actions, QVariantMap(), timeout); - if(reply.isValid()) { - uint dbusid = reply.value(); - _notificationsIdMap.insert(dbusid, notificationId); - _lastNotificationsDBusId = dbusid; +void StatusNotifierItem::showMessage(const QString& title, const QString& message_, SystemTray::MessageIcon icon, int timeout, uint notificationId) +{ + QString message = message_; + if (_notificationsClient->isValid()) { + if (_notificationsClientSupportsMarkup) { + message = message.toHtmlEscaped(); + } + + QStringList actions; + if (_notificationsClientSupportsActions) + actions << "activate" + << "View"; + + // we always queue notifications right now + QDBusReply reply = _notificationsClient->Notify(title, 0, "quassel", title, message, actions, QVariantMap(), timeout); + if (reply.isValid()) { + uint dbusid = reply.value(); + _notificationsIdMap.insert(dbusid, notificationId); + _lastNotificationsDBusId = dbusid; + } } - } else - StatusNotifierItemParent::showMessage(title, message, icon, timeout, notificationId); + else + StatusNotifierItemParent::showMessage(title, message, icon, timeout, notificationId); } -void StatusNotifierItem::closeMessage(uint notificationId) { - foreach(uint dbusid, _notificationsIdMap.keys()) { - if(_notificationsIdMap.value(dbusid) == notificationId) { - _notificationsIdMap.remove(dbusid); - _notificationsClient->CloseNotification(dbusid); +void StatusNotifierItem::closeMessage(uint notificationId) +{ + for (auto&& dbusid : _notificationsIdMap.keys()) { + if (_notificationsIdMap.value(dbusid) == notificationId) { + _notificationsIdMap.remove(dbusid); + _notificationsClient->CloseNotification(dbusid); + } } - } - _lastNotificationsDBusId = 0; + _lastNotificationsDBusId = 0; + + StatusNotifierItemParent::closeMessage(notificationId); } -void StatusNotifierItem::notificationClosed(uint dbusid, uint reason) { - Q_UNUSED(reason) - _lastNotificationsDBusId = 0; - emit messageClosed(_notificationsIdMap.take(dbusid)); +void StatusNotifierItem::notificationClosed(uint dbusid, uint reason) +{ + Q_UNUSED(reason) + _lastNotificationsDBusId = 0; + emit messageClosed(_notificationsIdMap.take(dbusid)); } -void StatusNotifierItem::notificationInvoked(uint dbusid, const QString &action) { - Q_UNUSED(action) - emit messageClicked(_notificationsIdMap.value(dbusid, 0)); +void StatusNotifierItem::notificationInvoked(uint dbusid, const QString& action) +{ + Q_UNUSED(action) + emit messageClicked(_notificationsIdMap.value(dbusid, 0)); } #endif