diff --git a/src/kstatusnotifieritem.cpp b/src/kstatusnotifieritem.cpp index 2f997d9..c79d71d0 100644 --- a/src/kstatusnotifieritem.cpp +++ b/src/kstatusnotifieritem.cpp @@ -1,1153 +1,1159 @@ /* This file is part of the KDE libraries Copyright 2009 by Marco Martin This library 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 library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kstatusnotifieritem.h" #include "kstatusnotifieritemprivate_p.h" #include "kstatusnotifieritemdbus_p.h" #include "debug_p.h" #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_MACOS #include #include #endif #include #include #include #include static const char s_statusNotifierWatcherServiceName[] = "org.kde.StatusNotifierWatcher"; static const int s_legacyTrayIconSize = 24; #if HAVE_DBUSMENUQT #include #endif //HAVE_DBUSMENUQT KStatusNotifierItem::KStatusNotifierItem(QObject *parent) : QObject(parent), d(new KStatusNotifierItemPrivate(this)) { d->init(QString()); } KStatusNotifierItem::KStatusNotifierItem(const QString &id, QObject *parent) : QObject(parent), d(new KStatusNotifierItemPrivate(this)) { d->init(id); } KStatusNotifierItem::~KStatusNotifierItem() { delete d->statusNotifierWatcher; delete d->notificationsClient; delete d->systemTrayIcon; if (!qApp->closingDown()) { delete d->menu; } if (d->associatedWidget) { KWindowSystem::self()->disconnect(d->associatedWidget); } delete d; } QString KStatusNotifierItem::id() const { //qCDebug(LOG_KNOTIFICATIONS) << "id requested" << d->id; return d->id; } void KStatusNotifierItem::setCategory(const ItemCategory category) { d->category = category; } KStatusNotifierItem::ItemStatus KStatusNotifierItem::status() const { return d->status; } KStatusNotifierItem::ItemCategory KStatusNotifierItem::category() const { return d->category; } void KStatusNotifierItem::setTitle(const QString &title) { d->title = title; } void KStatusNotifierItem::setStatus(const ItemStatus status) { if (d->status == status) { return; } d->status = status; emit d->statusNotifierItemDBus->NewStatus(QString::fromLatin1(metaObject()->enumerator(metaObject()->indexOfEnumerator("ItemStatus")).valueToKey(d->status))); if (d->systemTrayIcon) { d->syncLegacySystemTrayIcon(); } } //normal icon void KStatusNotifierItem::setIconByName(const QString &name) { if (d->iconName == name) { return; } d->serializedIcon = KDbusImageVector(); d->iconName = name; emit d->statusNotifierItemDBus->NewIcon(); if (d->systemTrayIcon) { d->systemTrayIcon->setIcon(QIcon::fromTheme(name)); } } QString KStatusNotifierItem::iconName() const { return d->iconName; } void KStatusNotifierItem::setIconByPixmap(const QIcon &icon) { if (d->iconName.isEmpty() && d->icon.cacheKey() == icon.cacheKey()) { return; } d->iconName.clear(); d->serializedIcon = d->iconToVector(icon); emit d->statusNotifierItemDBus->NewIcon(); d->icon = icon; if (d->systemTrayIcon) { d->systemTrayIcon->setIcon(icon); } } QIcon KStatusNotifierItem::iconPixmap() const { return d->icon; } void KStatusNotifierItem::setOverlayIconByName(const QString &name) { if (d->overlayIconName == name) { return; } d->overlayIconName = name; emit d->statusNotifierItemDBus->NewOverlayIcon(); if (d->systemTrayIcon) { QPixmap iconPixmap = QIcon::fromTheme(d->iconName).pixmap(s_legacyTrayIconSize, s_legacyTrayIconSize); if (!name.isEmpty()) { QPixmap overlayPixmap = QIcon::fromTheme(d->overlayIconName).pixmap(s_legacyTrayIconSize / 2, s_legacyTrayIconSize / 2); QPainter p(&iconPixmap); p.drawPixmap(iconPixmap.width() - overlayPixmap.width(), iconPixmap.height() - overlayPixmap.height(), overlayPixmap); p.end(); } d->systemTrayIcon->setIcon(iconPixmap); } } QString KStatusNotifierItem::overlayIconName() const { return d->overlayIconName; } void KStatusNotifierItem::setOverlayIconByPixmap(const QIcon &icon) { if (d->overlayIconName.isEmpty() && d->overlayIcon.cacheKey() == icon.cacheKey()) { return; } d->overlayIconName.clear(); d->serializedOverlayIcon = d->iconToVector(icon); emit d->statusNotifierItemDBus->NewOverlayIcon(); d->overlayIcon = icon; if (d->systemTrayIcon) { QPixmap iconPixmap = d->icon.pixmap(s_legacyTrayIconSize, s_legacyTrayIconSize); QPixmap overlayPixmap = d->overlayIcon.pixmap(s_legacyTrayIconSize / 2, s_legacyTrayIconSize / 2); QPainter p(&iconPixmap); p.drawPixmap(iconPixmap.width() - overlayPixmap.width(), iconPixmap.height() - overlayPixmap.height(), overlayPixmap); p.end(); d->systemTrayIcon->setIcon(iconPixmap); } } QIcon KStatusNotifierItem::overlayIconPixmap() const { return d->overlayIcon; } //Icons and movie for requesting attention state void KStatusNotifierItem::setAttentionIconByName(const QString &name) { if (d->attentionIconName == name) { return; } d->serializedAttentionIcon = KDbusImageVector(); d->attentionIconName = name; emit d->statusNotifierItemDBus->NewAttentionIcon(); } QString KStatusNotifierItem::attentionIconName() const { return d->attentionIconName; } void KStatusNotifierItem::setAttentionIconByPixmap(const QIcon &icon) { if (d->attentionIconName.isEmpty() && d->attentionIcon.cacheKey() == icon.cacheKey()) { return; } d->attentionIconName.clear(); d->serializedAttentionIcon = d->iconToVector(icon); d->attentionIcon = icon; emit d->statusNotifierItemDBus->NewAttentionIcon(); } QIcon KStatusNotifierItem::attentionIconPixmap() const { return d->attentionIcon; } void KStatusNotifierItem::setAttentionMovieByName(const QString &name) { if (d->movieName == name) { return; } d->movieName = name; delete d->movie; d->movie = nullptr; emit d->statusNotifierItemDBus->NewAttentionIcon(); if (d->systemTrayIcon) { d->movie = new QMovie(d->movieName); d->systemTrayIcon->setMovie(d->movie); } } QString KStatusNotifierItem::attentionMovieName() const { return d->movieName; } //ToolTip #ifdef Q_OS_MACOS static void setTrayToolTip(KStatusNotifierLegacyIcon *systemTrayIcon, const QString &title, const QString &subTitle) { if (systemTrayIcon) { bool tEmpty = title.isEmpty(), stEmpty = subTitle.isEmpty(); if (tEmpty) { if (!stEmpty) { systemTrayIcon->setToolTip(subTitle); } else { systemTrayIcon->setToolTip(title); } } else { if (stEmpty) { systemTrayIcon->setToolTip(title); } else { systemTrayIcon->setToolTip(title + QStringLiteral("\n") + subTitle); } } } } #else static void setTrayToolTip(KStatusNotifierLegacyIcon *systemTrayIcon, const QString &title, const QString &) { if (systemTrayIcon) { systemTrayIcon->setToolTip(title); } } #endif void KStatusNotifierItem::setToolTip(const QString &iconName, const QString &title, const QString &subTitle) { if (d->toolTipIconName == iconName && d->toolTipTitle == title && d->toolTipSubTitle == subTitle) { return; } d->serializedToolTipIcon = KDbusImageVector(); d->toolTipIconName = iconName; d->toolTipTitle = title; // if (d->systemTrayIcon) { // d->systemTrayIcon->setToolTip(title); // } setTrayToolTip(d->systemTrayIcon, title, subTitle); d->toolTipSubTitle = subTitle; emit d->statusNotifierItemDBus->NewToolTip(); } void KStatusNotifierItem::setToolTip(const QIcon &icon, const QString &title, const QString &subTitle) { if (d->toolTipIconName.isEmpty() && d->toolTipIcon.cacheKey() == icon.cacheKey() && d->toolTipTitle == title && d->toolTipSubTitle == subTitle) { return; } d->toolTipIconName.clear(); d->serializedToolTipIcon = d->iconToVector(icon); d->toolTipIcon = icon; d->toolTipTitle = title; // if (d->systemTrayIcon) { // d->systemTrayIcon->setToolTip(title); // } setTrayToolTip(d->systemTrayIcon, title, subTitle); d->toolTipSubTitle = subTitle; emit d->statusNotifierItemDBus->NewToolTip(); } void KStatusNotifierItem::setToolTipIconByName(const QString &name) { if (d->toolTipIconName == name) { return; } d->serializedToolTipIcon = KDbusImageVector(); d->toolTipIconName = name; emit d->statusNotifierItemDBus->NewToolTip(); } QString KStatusNotifierItem::toolTipIconName() const { return d->toolTipIconName; } void KStatusNotifierItem::setToolTipIconByPixmap(const QIcon &icon) { if (d->toolTipIconName.isEmpty() && d->toolTipIcon.cacheKey() == icon.cacheKey()) { return; } d->toolTipIconName.clear(); d->serializedToolTipIcon = d->iconToVector(icon); d->toolTipIcon = icon; emit d->statusNotifierItemDBus->NewToolTip(); } QIcon KStatusNotifierItem::toolTipIconPixmap() const { return d->toolTipIcon; } void KStatusNotifierItem::setToolTipTitle(const QString &title) { if (d->toolTipTitle == title) { return; } d->toolTipTitle = title; emit d->statusNotifierItemDBus->NewToolTip(); // if (d->systemTrayIcon) { // d->systemTrayIcon->setToolTip(title); // } setTrayToolTip(d->systemTrayIcon, title, d->toolTipSubTitle); } QString KStatusNotifierItem::toolTipTitle() const { return d->toolTipTitle; } void KStatusNotifierItem::setToolTipSubTitle(const QString &subTitle) { if (d->toolTipSubTitle == subTitle) { return; } d->toolTipSubTitle = subTitle; #ifdef Q_OS_MACOS setTrayToolTip(d->systemTrayIcon, d->toolTipTitle, subTitle); #endif emit d->statusNotifierItemDBus->NewToolTip(); } QString KStatusNotifierItem::toolTipSubTitle() const { return d->toolTipSubTitle; } void KStatusNotifierItem::setContextMenu(QMenu *menu) { if (d->menu && d->menu != menu) { d->menu->removeEventFilter(this); delete d->menu; } if (!menu) { d->menu = nullptr; return; } if (d->systemTrayIcon) { d->systemTrayIcon->setContextMenu(menu); } else if (d->menu != menu) { if (getenv("KSNI_NO_DBUSMENU")) { // This is a hack to make it possible to disable DBusMenu in an // application. The string "/NO_DBUSMENU" must be the same as in // DBusSystemTrayWidget::findDBusMenuInterface() in the Plasma // systemtray applet. d->menuObjectPath = QStringLiteral("/NO_DBUSMENU"); menu->installEventFilter(this); } else { d->menuObjectPath = QStringLiteral("/MenuBar"); #if HAVE_DBUSMENUQT new DBusMenuExporter(d->menuObjectPath, menu, d->statusNotifierItemDBus->dbusConnection()); #endif } connect(menu, SIGNAL(aboutToShow()), this, SLOT(contextMenuAboutToShow())); } d->menu = menu; Qt::WindowFlags oldFlags = d->menu->windowFlags(); d->menu->setParent(nullptr); d->menu->setWindowFlags(oldFlags); } QMenu *KStatusNotifierItem::contextMenu() const { return d->menu; } void KStatusNotifierItem::setAssociatedWidget(QWidget *associatedWidget) { if (associatedWidget) { d->associatedWidget = associatedWidget->window(); d->associatedWidgetPos = QPoint(-1, -1); QObject::connect(KWindowSystem::self(), &KWindowSystem::windowAdded, d->associatedWidget, [this](WId id) { if(d->associatedWidget->winId() == id && d->associatedWidgetPos != QPoint(-1, -1)) { d->associatedWidget->move(d->associatedWidgetPos); } } ); QObject::connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, d->associatedWidget, [this](WId id) { if(d->associatedWidget->winId() == id) { d->associatedWidgetPos = d->associatedWidget->pos(); } } ); } else if (d->associatedWidget) { KWindowSystem::self()->disconnect(d->associatedWidget); d->associatedWidget = nullptr; } if (d->systemTrayIcon) { delete d->systemTrayIcon; d->systemTrayIcon = nullptr; d->setLegacySystemTrayEnabled(true); } if (d->associatedWidget && d->associatedWidget != d->menu) { QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore")); if (!action) { action = new QAction(this); d->actionCollection.insert(QStringLiteral("minimizeRestore"), action); action->setText(tr("&Minimize")); connect(action, SIGNAL(triggered(bool)), this, SLOT(minimizeRestore())); } KWindowInfo info(d->associatedWidget->winId(), NET::WMDesktop); d->onAllDesktops = info.onAllDesktops(); } else { if (d->menu && d->hasQuit) { QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore")); if (action) { d->menu->removeAction(action); } } d->onAllDesktops = false; } } QWidget *KStatusNotifierItem::associatedWidget() const { return d->associatedWidget; } QList KStatusNotifierItem::actionCollection() const { return d->actionCollection.values(); } void KStatusNotifierItem::addAction(const QString &name, QAction *action) { d->actionCollection.insert(name, action); } void KStatusNotifierItem::removeAction(const QString &name) { d->actionCollection.remove(name); } QAction* KStatusNotifierItem::action(const QString &name) const { return d->actionCollection.value(name); } void KStatusNotifierItem::setStandardActionsEnabled(bool enabled) { if (d->standardActionsEnabled == enabled) { return; } d->standardActionsEnabled = enabled; if (d->menu && !enabled && d->hasQuit) { QAction *action = d->actionCollection.value(QStringLiteral("minimizeRestore")); if (action) { d->menu->removeAction(action); } action = d->actionCollection.value(QStringLiteral("quit")); if (action) { d->menu->removeAction(action); } d->hasQuit = false; } } bool KStatusNotifierItem::standardActionsEnabled() const { return d->standardActionsEnabled; } void KStatusNotifierItem::showMessage(const QString &title, const QString &message, const QString &icon, int timeout) { if (!d->notificationsClient) { d->notificationsClient = new org::freedesktop::Notifications(QStringLiteral("org.freedesktop.Notifications"), QStringLiteral("/org/freedesktop/Notifications"), QDBusConnection::sessionBus()); } uint id = 0; #ifdef Q_OS_MACOS if (d->systemTrayIcon) { // Growl is not needed anymore for QSystemTrayIcon::showMessage() since OS X 10.8 d->systemTrayIcon->showMessage(title, message, QSystemTrayIcon::Information, timeout); } else #endif { QVariantMap hints; - if (!qApp->desktopFileName().isEmpty()) { - hints.insert(QStringLiteral("desktop-entry"), qApp->desktopFileName()); + QString desktopFileName = QGuiApplication::desktopFileName(); + if (!desktopFileName.isEmpty()) { + // handle apps which set the desktopFileName property with filename suffix, + // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521) + if (desktopFileName.endsWith(QLatin1String(".desktop"))) { + desktopFileName.chop(8); + } + hints.insert(QStringLiteral("desktop-entry"), desktopFileName); } d->notificationsClient->Notify(d->title, id, icon, title, message, QStringList(), hints, timeout); } } QString KStatusNotifierItem::title() const { return d->title; } void KStatusNotifierItem::activate(const QPoint &pos) { //if the user activated the icon the NeedsAttention state is no longer necessary //FIXME: always true? if (d->status == NeedsAttention) { d->status = Active; #ifdef Q_OS_MACOS QtMac::setBadgeLabelText(QString()); #endif emit d->statusNotifierItemDBus->NewStatus(QString::fromLatin1(metaObject()->enumerator(metaObject()->indexOfEnumerator("ItemStatus")).valueToKey(d->status))); } if (d->associatedWidget == d->menu) { d->statusNotifierItemDBus->ContextMenu(pos.x(), pos.y()); return; } if (d->menu->isVisible()) { d->menu->hide(); } if (!d->associatedWidget) { emit activateRequested(true, pos); return; } d->checkVisibility(pos); } bool KStatusNotifierItemPrivate::checkVisibility(QPoint pos, bool perform) { #ifdef Q_OS_WIN #if 0 // the problem is that we lose focus when the systray icon is activated // and we don't know the former active window // therefore we watch for activation event and use our stopwatch :) if (GetTickCount() - dwTickCount < 300) { // we were active in the last 300ms -> hide it minimizeRestore(false); emit activateRequested(false, pos); } else { minimizeRestore(true); emit activateRequested(true, pos); } #endif #else // mapped = visible (but possibly obscured) const bool mapped = associatedWidget->isVisible() && !associatedWidget->isMinimized(); // - not mapped -> show, raise, focus // - mapped // - obscured -> raise, focus // - not obscured -> hide //info1.mappingState() != NET::Visible -> window on another desktop? if (!mapped) { if (perform) { minimizeRestore(true); emit q->activateRequested(true, pos); } return true; } else if (QGuiApplication::platformName() == QLatin1String("xcb")) { const KWindowInfo info1(associatedWidget->winId(), NET::XAWMState | NET::WMState | NET::WMDesktop); QListIterator< WId > it(KWindowSystem::stackingOrder()); it.toBack(); while (it.hasPrevious()) { WId id = it.previous(); if (id == associatedWidget->winId()) { break; } KWindowInfo info2(id, NET::WMDesktop | NET::WMGeometry | NET::XAWMState | NET::WMState | NET::WMWindowType); if (info2.mappingState() != NET::Visible) { continue; // not visible on current desktop -> ignore } if (!info2.geometry().intersects(associatedWidget->geometry())) { continue; // not obscuring the window -> ignore } if (!info1.hasState(NET::KeepAbove) && info2.hasState(NET::KeepAbove)) { continue; // obscured by window kept above -> ignore } NET::WindowType type = info2.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask); if (type == NET::Dock || type == NET::TopMenu) { continue; // obscured by dock or topmenu -> ignore } if (perform) { KWindowSystem::raiseWindow(associatedWidget->winId()); KWindowSystem::forceActiveWindow(associatedWidget->winId()); emit q->activateRequested(true, pos); } return true; } //not on current desktop? if (!info1.isOnCurrentDesktop()) { if (perform) { KWindowSystem::activateWindow(associatedWidget->winId()); emit q->activateRequested(true, pos); } return true; } if (perform) { minimizeRestore(false); // hide emit q->activateRequested(false, pos); } return false; } else { if (perform) { minimizeRestore(false); // hide emit q->activateRequested(false, pos); } return false; } #endif return true; } bool KStatusNotifierItem::eventFilter(QObject *watched, QEvent *event) { if (d->systemTrayIcon == nullptr) { //FIXME: ugly ugly workaround to weird QMenu's focus problems if (watched == d->menu && (event->type() == QEvent::WindowDeactivate || (event->type() == QEvent::MouseButtonRelease && static_cast(event)->button() == Qt::LeftButton))) { //put at the back of even queue to let the action activate anyways QTimer::singleShot(0, this, [this]() { d->hideMenu(); }); } } return false; } //KStatusNotifierItemPrivate const int KStatusNotifierItemPrivate::s_protocolVersion = 0; KStatusNotifierItemPrivate::KStatusNotifierItemPrivate(KStatusNotifierItem *item) : q(item), category(KStatusNotifierItem::ApplicationStatus), status(KStatusNotifierItem::Passive), movie(nullptr), menu(nullptr), associatedWidget(nullptr), titleAction(nullptr), statusNotifierWatcher(nullptr), notificationsClient(nullptr), systemTrayIcon(nullptr), hasQuit(false), onAllDesktops(false), standardActionsEnabled(true) { } void KStatusNotifierItemPrivate::init(const QString &extraId) { qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); statusNotifierItemDBus = new KStatusNotifierItemDBus(q); q->setAssociatedWidget(qobject_cast(q->parent())); QDBusServiceWatcher *watcher = new QDBusServiceWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, q); QObject::connect(watcher, SIGNAL(serviceOwnerChanged(QString,QString,QString)), q, SLOT(serviceChange(QString,QString,QString))); //create a default menu, just like in KSystemtrayIcon QMenu *m = new QMenu(associatedWidget); title = QGuiApplication::applicationDisplayName(); if (title.isEmpty()) { title = QCoreApplication::applicationName(); } #ifdef Q_OS_MACOS // OS X doesn't have texted separators so we emulate QAction::addSection(): // we first add an action with the desired text (title) and icon titleAction = m->addAction(qApp->windowIcon(), title); // this action should be disabled titleAction->setEnabled(false); // Give the titleAction a visible menu icon: // Systray icon and menu ("menu extra") are often used by applications that provide no other interface. // It is thus reasonable to show the application icon in the menu; Finder, Dock and App Switcher // all show it in addition to the application name (and Apple's input "menu extra" also shows icons). titleAction->setIconVisibleInMenu(true); m->addAction(titleAction); // now add a regular separator m->addSeparator(); #else titleAction = m->addSection(qApp->windowIcon(), title); m->setTitle(title); #endif q->setContextMenu(m); QAction *action = new QAction(q); action->setText(KStatusNotifierItem::tr("Quit")); action->setIcon(QIcon::fromTheme(QStringLiteral("application-exit"))); // cannot yet convert to function-pointer-based connect: // some apps like kalarm or korgac have a hack to rewire the connection // of the "quit" action to a own slot, and rely on the name-based slot to disconnect // TODO: extend KStatusNotifierItem API to support such needs QObject::connect(action, SIGNAL(triggered()), q, SLOT(maybeQuit())); actionCollection.insert(QStringLiteral("quit"), action); id = title; if (!extraId.isEmpty()) { id.append(QLatin1Char('_')).append(extraId); } // Init iconThemePath to the app folder for now iconThemePath = QStandardPaths::locate(QStandardPaths::DataLocation, QStringLiteral("icons"), QStandardPaths::LocateDirectory); registerToDaemon(); } void KStatusNotifierItemPrivate::registerToDaemon() { qCDebug(LOG_KNOTIFICATIONS) << "Registering a client interface to the KStatusNotifierWatcher"; if (!statusNotifierWatcher) { statusNotifierWatcher = new org::kde::StatusNotifierWatcher(QString::fromLatin1(s_statusNotifierWatcherServiceName), QStringLiteral("/StatusNotifierWatcher"), QDBusConnection::sessionBus()); } if (statusNotifierWatcher->isValid()) { // get protocol version in async way QDBusMessage msg = QDBusMessage::createMethodCall(QString::fromLatin1(s_statusNotifierWatcherServiceName), QStringLiteral("/StatusNotifierWatcher"), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Get")); msg.setArguments(QVariantList{QStringLiteral("org.kde.StatusNotifierWatcher"), QStringLiteral("ProtocolVersion")}); QDBusPendingCall async = QDBusConnection::sessionBus().asyncCall(msg); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, q); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, [this, watcher] { watcher->deleteLater(); QDBusPendingReply reply = *watcher; if (reply.isError()) { qCDebug(LOG_KNOTIFICATIONS) << "Failed to read protocol version of KStatusNotifierWatcher"; setLegacySystemTrayEnabled(true); } else { bool ok = false; const int protocolVersion = reply.value().toInt(&ok); if (ok && protocolVersion == s_protocolVersion) { statusNotifierWatcher->RegisterStatusNotifierItem(statusNotifierItemDBus->service()); setLegacySystemTrayEnabled(false); } else { qCDebug(LOG_KNOTIFICATIONS) << "KStatusNotifierWatcher has incorrect protocol version"; setLegacySystemTrayEnabled(true); } } } ); } else { qCDebug(LOG_KNOTIFICATIONS) << "KStatusNotifierWatcher not reachable"; setLegacySystemTrayEnabled(true); } } void KStatusNotifierItemPrivate::serviceChange(const QString &name, const QString &oldOwner, const QString &newOwner) { Q_UNUSED(name) if (newOwner.isEmpty()) { //unregistered qCDebug(LOG_KNOTIFICATIONS) << "Connection to the KStatusNotifierWatcher lost"; setLegacyMode(true); delete statusNotifierWatcher; statusNotifierWatcher = nullptr; } else if (oldOwner.isEmpty()) { //registered setLegacyMode(false); } } void KStatusNotifierItemPrivate::setLegacyMode(bool legacy) { if (legacy) { //unregistered setLegacySystemTrayEnabled(true); } else { //registered registerToDaemon(); } } void KStatusNotifierItemPrivate::legacyWheelEvent(int delta) { statusNotifierItemDBus->Scroll(delta, QStringLiteral("vertical")); } void KStatusNotifierItemPrivate::legacyActivated(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::MiddleClick) { emit q->secondaryActivateRequested(systemTrayIcon->geometry().topLeft()); } else if (reason == QSystemTrayIcon::Trigger) { q->activate(systemTrayIcon->geometry().topLeft()); } } void KStatusNotifierItemPrivate::setLegacySystemTrayEnabled(bool enabled) { if (enabled == (systemTrayIcon != nullptr)) { // already in the correct state return; } if (enabled) { bool isKde = !qEnvironmentVariableIsEmpty("KDE_FULL_SESSION") || qgetenv("XDG_CURRENT_DESKTOP") == "KDE"; if (!systemTrayIcon && !isKde) { if (!QSystemTrayIcon::isSystemTrayAvailable()) { return; } systemTrayIcon = new KStatusNotifierLegacyIcon(associatedWidget); syncLegacySystemTrayIcon(); systemTrayIcon->setToolTip(toolTipTitle); systemTrayIcon->show(); QObject::connect(systemTrayIcon, SIGNAL(wheel(int)), q, SLOT(legacyWheelEvent(int))); QObject::connect(systemTrayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)), q, SLOT(legacyActivated(QSystemTrayIcon::ActivationReason))); } else if (isKde) { // prevent infinite recursion if the KDE platform plugin is loaded // but SNI is not available; see bug 350785 qCWarning(LOG_KNOTIFICATIONS) << "env says KDE is running but SNI unavailable -- check " "KDE_FULL_SESSION and XDG_CURRENT_DESKTOP"; return; } if (menu) { menu->setWindowFlags(Qt::Popup); } } else { delete systemTrayIcon; systemTrayIcon = nullptr; if (menu) { menu->setWindowFlags(Qt::Window); } } if (menu) { QMenu *m = menu; menu = nullptr; q->setContextMenu(m); } } void KStatusNotifierItemPrivate::syncLegacySystemTrayIcon() { if (status == KStatusNotifierItem::NeedsAttention) { #ifdef Q_OS_MACOS QtMac::setBadgeLabelText(QString(QChar(0x26a0))/*QStringLiteral("!")*/); if (attentionIconName.isNull() && attentionIcon.isNull()) { // code adapted from kmail's KMSystemTray::updateCount() int overlaySize = 22; QIcon attnIcon = qApp->windowIcon(); if (!attnIcon.availableSizes().isEmpty()) { overlaySize = attnIcon.availableSizes().at(0).width(); } QFont labelFont = QFontDatabase::systemFont(QFontDatabase::GeneralFont); labelFont.setBold(true); QFontMetrics qfm(labelFont); float attnHeight = overlaySize * 0.667; if (qfm.height() > attnHeight) { float labelSize = attnHeight; labelFont.setPointSizeF(labelSize); } // Paint the label in a pixmap QPixmap overlayPixmap(overlaySize, overlaySize); overlayPixmap.fill(Qt::transparent); QPainter p(&overlayPixmap); p.setFont(labelFont); p.setBrush(Qt::NoBrush); // this sort of badge/label is red on OS X p.setPen(QColor(224,0,0)); p.setOpacity(1.0); // use U+2022, the Unicode bullet p.drawText(overlayPixmap.rect(), Qt::AlignRight|Qt::AlignTop, QString(QChar(0x2022))); p.end(); QPixmap iconPixmap = attnIcon.pixmap(overlaySize, overlaySize); QPainter pp(&iconPixmap); pp.drawPixmap(0, 0, overlayPixmap); pp.end(); systemTrayIcon->setIcon(iconPixmap); } else #endif { if (!movieName.isNull()) { if (!movie) { movie = new QMovie(movieName); } systemTrayIcon->setMovie(movie); } else if (!attentionIconName.isNull()) { systemTrayIcon->setIcon(QIcon::fromTheme(attentionIconName)); } else { systemTrayIcon->setIcon(attentionIcon); } } } else { #ifdef Q_OS_MACOS if (!iconName.isNull()) { QIcon theIcon = QIcon::fromTheme(iconName); systemTrayIcon->setIconWithMask(theIcon, status==KStatusNotifierItem::Passive); } else { systemTrayIcon->setIconWithMask(icon, status==KStatusNotifierItem::Passive); } QtMac::setBadgeLabelText(QString()); #else if (!iconName.isNull()) { systemTrayIcon->setIcon(QIcon::fromTheme(iconName)); } else { systemTrayIcon->setIcon(icon); } #endif } systemTrayIcon->setToolTip(toolTipTitle); } void KStatusNotifierItemPrivate::contextMenuAboutToShow() { if (!hasQuit && standardActionsEnabled) { // we need to add the actions to the menu afterwards so that these items // appear at the _END_ of the menu menu->addSeparator(); if (associatedWidget && associatedWidget != menu) { QAction *action = actionCollection.value(QStringLiteral("minimizeRestore")); if (action) { menu->addAction(action); } } QAction *action = actionCollection.value(QStringLiteral("quit")); if (action) { menu->addAction(action); } hasQuit = true; } if (associatedWidget && associatedWidget != menu) { QAction *action = actionCollection.value(QStringLiteral("minimizeRestore")); if (checkVisibility(QPoint(0, 0), false)) { action->setText(KStatusNotifierItem::tr("&Restore")); } else { action->setText(KStatusNotifierItem::tr("&Minimize")); } } } void KStatusNotifierItemPrivate::maybeQuit() { QString caption = QGuiApplication::applicationDisplayName(); if (caption.isEmpty()) { caption = QCoreApplication::applicationName(); } QString query = KStatusNotifierItem::tr("Are you sure you want to quit %1?").arg(caption); if (QMessageBox::question(associatedWidget, KStatusNotifierItem::tr("Confirm Quit From System Tray"), query) == QMessageBox::Yes) { qApp->quit(); } } void KStatusNotifierItemPrivate::minimizeRestore() { q->activate(systemTrayIcon ? systemTrayIcon->geometry().topLeft() : QPoint(0, 0)); } void KStatusNotifierItemPrivate::hideMenu() { menu->hide(); } void KStatusNotifierItemPrivate::minimizeRestore(bool show) { KWindowInfo info(associatedWidget->winId(), NET::WMDesktop); if (show) { if (onAllDesktops) { KWindowSystem::setOnAllDesktops(associatedWidget->winId(), true); } else { KWindowSystem::setCurrentDesktop(info.desktop()); } auto state = associatedWidget->windowState() & ~Qt::WindowMinimized; associatedWidget->setWindowState(state); associatedWidget->show(); associatedWidget->raise(); } else { onAllDesktops = info.onAllDesktops(); associatedWidget->hide(); } } KDbusImageStruct KStatusNotifierItemPrivate::imageToStruct(const QImage &image) { KDbusImageStruct icon; icon.width = image.size().width(); icon.height = image.size().height(); if (image.format() == QImage::Format_ARGB32) { icon.data = QByteArray((char *)image.bits(), image.sizeInBytes()); } else { QImage image32 = image.convertToFormat(QImage::Format_ARGB32); icon.data = QByteArray((char *)image32.bits(), image32.sizeInBytes()); } //swap to network byte order if we are little endian if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) { quint32 *uintBuf = (quint32 *) icon.data.data(); for (uint i = 0; i < icon.data.size() / sizeof(quint32); ++i) { *uintBuf = qToBigEndian(*uintBuf); ++uintBuf; } } return icon; } KDbusImageVector KStatusNotifierItemPrivate::iconToVector(const QIcon &icon) { KDbusImageVector iconVector; QPixmap iconPixmap; //if an icon exactly that size wasn't found don't add it to the vector const auto lstSizes = icon.availableSizes(); for (QSize size : lstSizes) { iconPixmap = icon.pixmap(size); iconVector.append(imageToStruct(iconPixmap.toImage())); } return iconVector; } #include "moc_kstatusnotifieritem.cpp" #include "moc_kstatusnotifieritemprivate_p.cpp" diff --git a/src/notifybyaudio_canberra.cpp b/src/notifybyaudio_canberra.cpp index 1f9f177..4ed8a96 100644 --- a/src/notifybyaudio_canberra.cpp +++ b/src/notifybyaudio_canberra.cpp @@ -1,200 +1,206 @@ /* This file is part of the KDE libraries Copyright (C) 2014-2015 by Martin Klapetek Copyright (C) 2018 Kai Uwe Broulik This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "notifybyaudio_canberra.h" #include "debug_p.h" #include #include #include #include #include #include "knotifyconfig.h" #include "knotification.h" #include NotifyByAudio::NotifyByAudio(QObject *parent) : KNotificationPlugin(parent) { qRegisterMetaType("uint32_t"); int ret = ca_context_create(&m_context); if (ret != CA_SUCCESS) { qCWarning(LOG_KNOTIFICATIONS) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret); m_context = nullptr; return; } + QString desktopFileName = QGuiApplication::desktopFileName(); + // handle apps which set the desktopFileName property with filename suffix, + // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521) + if (desktopFileName.endsWith(QLatin1String(".desktop"))) { + desktopFileName.chop(8); + } ret = ca_context_change_props(m_context, CA_PROP_APPLICATION_NAME, qUtf8Printable(qApp->applicationDisplayName()), - CA_PROP_APPLICATION_ID, qUtf8Printable(qApp->desktopFileName()), + CA_PROP_APPLICATION_ID, qUtf8Printable(desktopFileName), CA_PROP_APPLICATION_ICON_NAME, qUtf8Printable(qApp->windowIcon().name()), nullptr); if (ret != CA_SUCCESS) { qCWarning(LOG_KNOTIFICATIONS) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret); } } NotifyByAudio::~NotifyByAudio() { if (m_context) { ca_context_destroy(m_context); } m_context = nullptr; } void NotifyByAudio::notify(KNotification *notification, KNotifyConfig *config) { const QString soundFilename = config->readEntry(QStringLiteral("Sound")); if (soundFilename.isEmpty()) { qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but no sound file provided in notifyrc file, aborting audio notification"; finish(notification); return; } QUrl soundURL; const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); for (const QString &dataLocation : dataLocations) { soundURL = QUrl::fromUserInput(soundFilename, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile); if (soundURL.isLocalFile() && QFileInfo::exists(soundURL.toLocalFile())) { break; } else if (!soundURL.isLocalFile() && soundURL.isValid()) { break; } soundURL.clear(); } if (soundURL.isEmpty()) { qCWarning(LOG_KNOTIFICATIONS) << "Audio notification requested, but sound file from notifyrc file was not found, aborting audio notification"; finish(notification); return; } // Looping happens in the finishCallback if (!playSound(m_currentId, soundURL)) { finish(notification); return; } if (notification->flags() & KNotification::LoopSound) { m_loopSoundUrls.insert(m_currentId, soundURL); } Q_ASSERT(!m_notifications.value(m_currentId)); m_notifications.insert(m_currentId, notification); ++m_currentId; } bool NotifyByAudio::playSound(quint32 id, const QUrl &url) { if (!m_context) { qCWarning(LOG_KNOTIFICATIONS) << "Cannot play notification sound without canberra context"; return false; } ca_proplist *props = nullptr; ca_proplist_create(&props); // We'll also want this cached for a time. volatile makes sure the cache is // dropped after some time or when the cache is under pressure. ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, QFile::encodeName(url.toLocalFile()).constData()); ca_proplist_sets(props, CA_PROP_CANBERRA_CACHE_CONTROL, "volatile"); int ret = ca_context_play_full(m_context, id, props, &ca_finish_callback, this); ca_proplist_destroy(props); if (ret != CA_SUCCESS) { qCWarning(LOG_KNOTIFICATIONS) << "Failed to play sound with canberra:" << ca_strerror(ret); return false; } return true; } void NotifyByAudio::ca_finish_callback(ca_context *c, uint32_t id, int error_code, void *userdata) { Q_UNUSED(c); QMetaObject::invokeMethod(static_cast(userdata), "finishCallback", Q_ARG(uint32_t, id), Q_ARG(int, error_code)); } void NotifyByAudio::finishCallback(uint32_t id, int error_code) { KNotification *notification = m_notifications.value(id, nullptr); if (!notification) { // We may have gotten a late finish callback. return; } if (error_code == CA_SUCCESS) { // Loop the sound now if we have one const QUrl soundUrl = m_loopSoundUrls.value(id); if (soundUrl.isValid()) { if (!playSound(id, soundUrl)) { finishNotification(notification, id); } return; } } else if (error_code != CA_ERROR_CANCELED) { qCWarning(LOG_KNOTIFICATIONS) << "Playing audio notification failed:" << ca_strerror(error_code); } finishNotification(notification, id); } void NotifyByAudio::close(KNotification *notification) { if (!m_notifications.values().contains(notification)) { return; } const auto id = m_notifications.key(notification); if (m_context) { int ret = ca_context_cancel(m_context, id); if (ret != CA_SUCCESS) { qCWarning(LOG_KNOTIFICATIONS) << "Failed to cancel canberra context for audio notification:" << ca_strerror(ret); return; } } // Consider the notification finished. ca_context_cancel schedules a cancel // but we need to stop using the noficiation immediately or we could access // a notification past its lifetime (close() may, or indeed must, // schedule deletion of the notification). // https://bugs.kde.org/show_bug.cgi?id=398695 finishNotification(notification, id); } void NotifyByAudio::finishNotification(KNotification *notification, quint32 id) { m_notifications.remove(id); m_loopSoundUrls.remove(id); finish(notification); } diff --git a/src/notifybypopup.cpp b/src/notifybypopup.cpp index 51a410e..c644c40 100644 --- a/src/notifybypopup.cpp +++ b/src/notifybypopup.cpp @@ -1,870 +1,876 @@ /* Copyright (C) 2005-2009 by Olivier Goffart Copyright (C) 2008 by Dmitry Suzdalev Copyright (C) 2014 by Martin Klapetek This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) version 3, or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 6 of version 3 of the license. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ #include "notifybypopup.h" #include "imageconverter.h" #include "notifybypopupgrowl.h" #include "kpassivepopup.h" #include "knotifyconfig.h" #include "knotification.h" #include "debug_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const char dbusServiceName[] = "org.freedesktop.Notifications"; static const char dbusInterfaceName[] = "org.freedesktop.Notifications"; static const char dbusPath[] = "/org/freedesktop/Notifications"; class NotifyByPopupPrivate { public: NotifyByPopupPrivate(NotifyByPopup *parent) : q(parent) {} /** * @internal * Fills the KPassivePopup with data */ void fillPopup(KPassivePopup *popup, KNotification *notification, const KNotifyConfig &config); /** * Removes HTML from a given string. Replaces line breaks with \n and * HTML entities by their 'normal forms'. * @param string the HTML to remove. * @return the cleaned string. */ QString stripHtml(const QString &text); /** * Sends notification to DBus "org.freedesktop.notifications" interface. * @param id knotify-sid identifier of notification * @param config notification data * @param update If true, will request the DBus service to update the notification with new data from \c notification * Otherwise will put new notification on screen * @return true for success or false if there was an error. */ bool sendNotificationToGalagoServer(KNotification *notification, const KNotifyConfig &config, bool update = false); /** * Sends request to close Notification with id to DBus "org.freedesktop.notifications" interface * @param id knotify-side notification ID to close */ void closeGalagoNotification(KNotification *notification); /** * Find the caption and the icon name of the application */ void getAppCaptionAndIconName(const KNotifyConfig &config, QString *appCaption, QString *iconName); /* * Query the dbus server for notification capabilities * If no DBus server is present, use fallback capabilities for KPassivePopup */ void queryPopupServerCapabilities(); // the y coordinate of the next position popup should appears int nextPosition; int animationTimer; /** * Specifies if DBus Notifications interface exists on session bus */ bool dbusServiceExists; bool dbusServiceActivatable; /** * DBus notification daemon capabilities cache. * Do not use this variable. Use #popupServerCapabilities() instead. * @see popupServerCapabilities */ QStringList popupServerCapabilities; /** * In case we still don't know notification server capabilities, * we need to query those first. That's done in an async way * so we queue all notifications while waiting for the capabilities * to return, then process them from this queue */ QList > notificationQueue; /** * Whether the DBus notification daemon capability cache is up-to-date. */ bool dbusServiceCapCacheDirty; /** * Keeps the map of notifications done in KPassivePopup */ QMap passivePopups; /* * As we communicate with the notification server over dbus * we use only ids, this is for fast KNotifications lookup */ QHash> galagoNotifications; NotifyByPopup * const q; /** * A class for resolving HTML entities in XML documents (used * during HTML stripping) */ class HtmlEntityResolver : public QXmlStreamEntityResolver { QString resolveUndeclaredEntity(const QString &name) override; }; }; //--------------------------------------------------------------------------------------- NotifyByPopup::NotifyByPopup(QObject *parent) : KNotificationPlugin(parent), d(new NotifyByPopupPrivate(this)) { d->animationTimer = 0; d->dbusServiceExists = false; d->dbusServiceActivatable = false; d->dbusServiceCapCacheDirty = true; d->nextPosition = -1; // check if service already exists on plugin instantiation QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); d->dbusServiceExists = interface && interface->isServiceRegistered(QString::fromLatin1(dbusServiceName)); if (d->dbusServiceExists) { onServiceOwnerChanged(QString::fromLatin1(dbusServiceName), QString(), QStringLiteral("_")); //connect signals } // to catch register/unregister events from service in runtime QDBusServiceWatcher *watcher = new QDBusServiceWatcher(this); watcher->setConnection(QDBusConnection::sessionBus()); watcher->setWatchMode(QDBusServiceWatcher::WatchForOwnerChange); watcher->addWatchedService(QString::fromLatin1(dbusServiceName)); connect(watcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &NotifyByPopup::onServiceOwnerChanged); #ifndef Q_WS_WIN if (!d->dbusServiceExists) { QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.DBus"), QStringLiteral("/org/freedesktop/DBus"), QStringLiteral("org.freedesktop.DBus"), QStringLiteral("ListActivatableNames")); QDBusReply reply = QDBusConnection::sessionBus().call(message); if (reply.isValid() && reply.value().contains(QString::fromLatin1(dbusServiceName))) { d->dbusServiceActivatable = true; //if the service is activatable, we can assume it exists even if it is not currently running d->dbusServiceExists = true; } } #endif } NotifyByPopup::~NotifyByPopup() { for (KPassivePopup *p : qAsConst(d->passivePopups)) { p->deleteLater(); } delete d; } void NotifyByPopup::notify(KNotification *notification, KNotifyConfig *notifyConfig) { notify(notification, *notifyConfig); } void NotifyByPopup::notify(KNotification *notification, const KNotifyConfig ¬ifyConfig) { if (d->passivePopups.contains(notification) || d->galagoNotifications.contains(notification->id())) { // notification is already on the screen, do nothing finish(notification); return; } // check if Notifications DBus service exists on bus, use it if it does if (d->dbusServiceExists) { if (d->dbusServiceCapCacheDirty) { // if we don't have the server capabilities yet, we need to query for them first; // as that is an async dbus operation, we enqueue the notification and process them // when we receive dbus reply with the server capabilities d->notificationQueue.append(qMakePair(notification, notifyConfig)); d->queryPopupServerCapabilities(); } else { if (!d->sendNotificationToGalagoServer(notification, notifyConfig)) { finish(notification); //an error occurred. } } return; } // Persistent => 0 == infinite timeout // CloseOnTimeout => -1 == let the server decide int timeout = (notification->flags() & KNotification::Persistent) ? 0 : -1; // if Growl can display our popups, use that instead if (NotifyByPopupGrowl::canPopup()) { QString appCaption, iconName; d->getAppCaptionAndIconName(notifyConfig, &appCaption, &iconName); appCaption = d->stripHtml(appCaption); //did the user override the icon name? if (!notification->iconName().isEmpty()) { iconName = notification->iconName(); } // Our growl implementation does not support html stuff // so strip it off right away QString text = notification->text(); text = d->stripHtml(text); // The first arg is QPixmap*, however that pixmap is not used // at all (it has Q_UNUSED) so just set it to 0 NotifyByPopupGrowl::popup(nullptr, timeout, appCaption, text); // Finish immediately, because current NotifyByPopupGrowl can't callback finish(notification); return; } // Check if this object lives in the GUI thread and return if it doesn't // as Qt cannot create/handle widgets in non-GUI threads if (QThread::currentThread() != qApp->thread()) { qCWarning(LOG_KNOTIFICATIONS) << "KNotification did not detect any running org.freedesktop.Notifications server and fallback notifications cannot be used from non-GUI thread!"; return; } if (!qobject_cast(QCoreApplication::instance())) { qCWarning(LOG_KNOTIFICATIONS) << "KNotification did not detect any running org.freedesktop.Notifications server and fallback notifications cannot be used without a QApplication!"; return; } // last fallback - display the popup using KPassivePopup KPassivePopup *pop = new KPassivePopup(notification->widget()); d->passivePopups.insert(notification, pop); d->fillPopup(pop, notification, notifyConfig); QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); if (d->nextPosition == -1) { d->nextPosition = screen.top(); } pop->setAutoDelete(true); connect(pop, &QObject::destroyed, this, &NotifyByPopup::onPassivePopupDestroyed); pop->setTimeout(timeout); pop->adjustSize(); pop->show(QPoint(screen.left() + screen.width()/2 - pop->width()/2 , d->nextPosition)); d->nextPosition += pop->height(); } void NotifyByPopup::onPassivePopupDestroyed() { const QObject *destroyedPopup = sender(); if (!destroyedPopup) { return; } for (QMap::iterator it = d->passivePopups.begin(); it != d->passivePopups.end(); ++it) { QObject *popup = it.value(); if (popup && popup == destroyedPopup) { finish(it.key()); d->passivePopups.remove(it.key()); break; } } //relocate popup if (!d->animationTimer) { d->animationTimer = startTimer(10); } } void NotifyByPopup::timerEvent(QTimerEvent *event) { if (event->timerId() != d->animationTimer) { KNotificationPlugin::timerEvent(event); return; } bool cont = false; QRect screen = QGuiApplication::primaryScreen()->availableGeometry(); d->nextPosition = screen.top(); for (KPassivePopup *popup : qAsConst(d->passivePopups)) { int y = popup->pos().y(); if (y > d->nextPosition) { y = qMax(y - 5, d->nextPosition); d->nextPosition = y + popup->height(); cont = cont || y != d->nextPosition; popup->move(popup->pos().x(), y); } else { d->nextPosition += popup->height(); } } if (!cont) { killTimer(d->animationTimer); d->animationTimer = 0; } } void NotifyByPopup::onPassivePopupLinkClicked(const QString &link) { unsigned int id = link.section(QLatin1Char('/') , 0 , 0).toUInt(); unsigned int action = link.section(QLatin1Char('/') , 1 , 1).toUInt(); if (id == 0 || action == 0) { return; } emit actionInvoked(id, action); } void NotifyByPopup::close(KNotification *notification) { if (d->dbusServiceExists) { d->closeGalagoNotification(notification); } if (d->passivePopups.contains(notification)) { // this will call onPassivePopupDestroyed() // which will call finish() on the notification d->passivePopups[notification]->deleteLater(); } QMutableListIterator > iter(d->notificationQueue); while (iter.hasNext()) { auto &item = iter.next(); if (item.first == notification) { iter.remove(); } } } void NotifyByPopup::update(KNotification *notification, KNotifyConfig *notifyConfig) { update(notification, *notifyConfig); } void NotifyByPopup::update(KNotification *notification, const KNotifyConfig ¬ifyConfig) { if (d->passivePopups.contains(notification)) { KPassivePopup *p = d->passivePopups[notification]; d->fillPopup(p, notification, notifyConfig); return; } // if Notifications DBus service exists on bus, // it'll be used instead if (d->dbusServiceExists) { d->sendNotificationToGalagoServer(notification, notifyConfig, true); return; } // otherwise, just display a new Growl notification if (NotifyByPopupGrowl::canPopup()) { notify(notification, notifyConfig); } } void NotifyByPopup::onServiceOwnerChanged(const QString &serviceName, const QString &oldOwner, const QString &newOwner) { Q_UNUSED(serviceName); // close all notifications we currently hold reference to for (KNotification *n : qAsConst(d->galagoNotifications)) { if (n) { emit finished(n); } } QMap::const_iterator i = d->passivePopups.constBegin(); while (i != d->passivePopups.constEnd()) { emit finished(i.key()); ++i; } d->galagoNotifications.clear(); d->passivePopups.clear(); d->dbusServiceCapCacheDirty = true; d->popupServerCapabilities.clear(); if (newOwner.isEmpty()) { d->notificationQueue.clear(); if (!d->dbusServiceActivatable) { d->dbusServiceExists = false; } } else if (oldOwner.isEmpty()) { d->dbusServiceExists = true; // connect to action invocation signals bool connected = QDBusConnection::sessionBus().connect(QString(), // from any service QString::fromLatin1(dbusPath), QString::fromLatin1(dbusInterfaceName), QStringLiteral("ActionInvoked"), this, SLOT(onGalagoNotificationActionInvoked(uint,QString))); if (!connected) { qCWarning(LOG_KNOTIFICATIONS) << "warning: failed to connect to ActionInvoked dbus signal"; } connected = QDBusConnection::sessionBus().connect(QString(), // from any service QString::fromLatin1(dbusPath), QString::fromLatin1(dbusInterfaceName), QStringLiteral("NotificationClosed"), this, SLOT(onGalagoNotificationClosed(uint,uint))); if (!connected) { qCWarning(LOG_KNOTIFICATIONS) << "warning: failed to connect to NotificationClosed dbus signal"; } } } void NotifyByPopup::onGalagoNotificationActionInvoked(uint notificationId, const QString &actionKey) { auto iter = d->galagoNotifications.find(notificationId); if (iter == d->galagoNotifications.end()) { return; } KNotification *n = *iter; if (n) { if (actionKey == QStringLiteral("default")) { emit actionInvoked(n->id(), 0); } else { emit actionInvoked(n->id(), actionKey.toUInt()); } } else { d->galagoNotifications.erase(iter); } } void NotifyByPopup::onGalagoNotificationClosed(uint dbus_id, uint reason) { auto iter = d->galagoNotifications.find(dbus_id); if (iter == d->galagoNotifications.end()) { return; } KNotification *n = *iter; d->galagoNotifications.remove(dbus_id); if (n) { emit finished(n); // The popup bubble is the only user facing part of a notification, // if the user closes the popup, it means he wants to get rid // of the notification completely, including playing sound etc // Therefore we close the KNotification completely after closing // the popup, but only if the reason is 2, which means "user closed" if (reason == 2) { n->close(); } } } void NotifyByPopup::onGalagoServerReply(QDBusPendingCallWatcher *watcher) { // call deleteLater first, since we might return in the middle of the function watcher->deleteLater(); KNotification *notification = watcher->property("notificationObject").value(); if (!notification) { qCWarning(LOG_KNOTIFICATIONS) << "Invalid notification object passed in DBus reply watcher; notification will probably break"; return; } QDBusPendingReply reply = *watcher; d->galagoNotifications.insert(reply.argumentAt<0>(), notification); } void NotifyByPopup::onGalagoServerCapabilitiesReceived(const QStringList &capabilities) { d->popupServerCapabilities = capabilities; d->dbusServiceCapCacheDirty = false; // re-run notify() on all enqueued notifications for (int i = 0, total = d->notificationQueue.size(); i < total; ++i) { notify(d->notificationQueue.at(i).first, d->notificationQueue.at(i).second); } d->notificationQueue.clear(); } void NotifyByPopupPrivate::getAppCaptionAndIconName(const KNotifyConfig ¬ifyConfig, QString *appCaption, QString *iconName) { KConfigGroup globalgroup(&(*notifyConfig.eventsfile), QStringLiteral("Global")); *appCaption = globalgroup.readEntry("Name", globalgroup.readEntry("Comment", notifyConfig.appname)); KConfigGroup eventGroup(&(*notifyConfig.eventsfile), QStringLiteral("Event/%1").arg(notifyConfig.eventid)); if (eventGroup.hasKey("IconName")) { *iconName = eventGroup.readEntry("IconName", notifyConfig.appname); } else { *iconName = globalgroup.readEntry("IconName", notifyConfig.appname); } } void NotifyByPopupPrivate::fillPopup(KPassivePopup *popup, KNotification *notification, const KNotifyConfig ¬ifyConfig) { QString appCaption; QString iconName; getAppCaptionAndIconName(notifyConfig, &appCaption, &iconName); // If we're at this place, it means there's no D-Bus service for notifications // so we don't need to do D-Bus query for the capabilities. // If queryPopupServerCapabilities() finds no service, it sets the KPassivePopup // capabilities immediately, so we don't need to wait for callback as in the case // of galago notifications queryPopupServerCapabilities(); int iconDimension = QFontMetrics(QFont()).height(); QPixmap appIcon = QIcon::fromTheme(iconName).pixmap(iconDimension, iconDimension); QWidget *vb = popup->standardView(notification->title().isEmpty() ? appCaption : notification->title(), notification->pixmap().isNull() ? notification->text() : QString(), appIcon); if (!notification->pixmap().isNull()) { const QPixmap pix = notification->pixmap(); QHBoxLayout *hbox = new QHBoxLayout(vb); QLabel *pil = new QLabel(); pil->setPixmap(pix); pil->setScaledContents(true); if (pix.height() > 80 && pix.height() > pix.width()) { pil->setMaximumHeight(80); pil->setMaximumWidth(80 * pix.width() / pix.height()); } else if(pix.width() > 80 && pix.height() <= pix.width()) { pil->setMaximumWidth(80); pil->setMaximumHeight(80*pix.height()/pix.width()); } hbox->addWidget(pil); QVBoxLayout *vb2 = new QVBoxLayout(vb); QLabel *msg = new QLabel(notification->text()); msg->setAlignment(Qt::AlignLeft); vb2->addWidget(msg); hbox->addLayout(vb2); vb->layout()->addItem(hbox); } if (!notification->actions().isEmpty()) { QString linkCode = QStringLiteral("

"); int i = 0; const auto actionList = notification->actions(); for (const QString &it : actionList) { i++; linkCode += QStringLiteral(" %3").arg(QString::number(notification->id()), QString::number(i), it.toHtmlEscaped()); } linkCode += QLatin1String("

"); QLabel *link = new QLabel(linkCode , vb ); link->setTextInteractionFlags(Qt::LinksAccessibleByMouse); link->setOpenExternalLinks(false); //link->setAlignment( AlignRight ); QObject::connect(link, &QLabel::linkActivated, q, &NotifyByPopup::onPassivePopupLinkClicked); QObject::connect(link, &QLabel::linkActivated, popup, &QWidget::hide); } popup->setView( vb ); } bool NotifyByPopupPrivate::sendNotificationToGalagoServer(KNotification *notification, const KNotifyConfig ¬ifyConfig_nocheck, bool update) { uint updateId = galagoNotifications.key(notification, 0); if (update) { if (updateId == 0) { // we have nothing to update; the notification we're trying to update // has been already closed return false; } } QDBusMessage dbusNotificationMessage = QDBusMessage::createMethodCall(QString::fromLatin1(dbusServiceName), QString::fromLatin1(dbusPath), QString::fromLatin1(dbusInterfaceName), QStringLiteral("Notify")); QList args; QString appCaption; QString iconName; getAppCaptionAndIconName(notifyConfig_nocheck, &appCaption, &iconName); //did the user override the icon name? if (!notification->iconName().isEmpty()) { iconName = notification->iconName(); } args.append(appCaption); // app_name args.append(updateId); // notification to update args.append(iconName); // app_icon QString title = notification->title().isEmpty() ? appCaption : notification->title(); QString text = notification->text(); if (!popupServerCapabilities.contains(QStringLiteral("body-markup"))) { if (title.startsWith(QLatin1String(""))) { title = stripHtml(title); } if (text.startsWith(QLatin1String(""))) { text = stripHtml(text); } } args.append(title); // summary args.append(text); // body // galago spec defines action list to be list like // (act_id1, action1, act_id2, action2, ...) // // assign id's to actions like it's done in fillPopup() method // (i.e. starting from 1) QStringList actionList; if (popupServerCapabilities.contains(QStringLiteral("actions"))) { QString defaultAction = notification->defaultAction(); if (!defaultAction.isEmpty()) { actionList.append(QStringLiteral("default")); actionList.append(defaultAction); } int actId = 0; const auto listActions = notification->actions(); for (const QString &actionName : listActions) { actId++; actionList.append(QString::number(actId)); actionList.append(actionName); } } args.append(actionList); // actions QVariantMap hintsMap; // Add the application name to the hints. // According to fdo spec, the app_name is supposed to be the applicaton's "pretty name" // but in some places it's handy to know the application name itself if (!notification->appName().isEmpty()) { hintsMap[QStringLiteral("x-kde-appname")] = notification->appName(); } if (!notification->eventId().isEmpty()) { hintsMap[QStringLiteral("x-kde-eventId")] = notification->eventId(); } if (notification->flags() & KNotification::SkipGrouping) { hintsMap[QStringLiteral("x-kde-skipGrouping")] = 1; } if (!notification->urls().isEmpty()) { hintsMap[QStringLiteral("x-kde-urls")] = QUrl::toStringList(notification->urls()); } if (!(notification->flags() & KNotification::Persistent)) { hintsMap[QStringLiteral("transient")] = true; } - if (!qApp->desktopFileName().isEmpty()) { - hintsMap[QStringLiteral("desktop-entry")] = qApp->desktopFileName(); + QString desktopFileName = QGuiApplication::desktopFileName(); + if (!desktopFileName.isEmpty()) { + // handle apps which set the desktopFileName property with filename suffix, + // due to unclear API dox (https://bugreports.qt.io/browse/QTBUG-75521) + if (desktopFileName.endsWith(QLatin1String(".desktop"))) { + desktopFileName.chop(8); + } + hintsMap[QStringLiteral("desktop-entry")] = desktopFileName; } int urgency = -1; switch (notification->urgency()) { case KNotification::DefaultUrgency: break; case KNotification::LowUrgency: urgency = 0; break; case KNotification::NormalUrgency: Q_FALLTHROUGH(); // galago notifications only know low, normal, critical case KNotification::HighUrgency: urgency = 1; break; case KNotification::CriticalUrgency: urgency = 2; break; } if (urgency > -1) { hintsMap[QStringLiteral("urgency")] = urgency; } const QVariantMap hints = notification->hints(); for (auto it = hints.constBegin(); it != hints.constEnd(); ++it) { hintsMap[it.key()] = it.value(); } //FIXME - reenable/fix // let's see if we've got an image, and store the image in the hints map if (!notification->pixmap().isNull()) { QByteArray pixmapData; QBuffer buffer(&pixmapData); buffer.open(QIODevice::WriteOnly); notification->pixmap().save(&buffer, "PNG"); buffer.close(); hintsMap[QStringLiteral("image_data")] = ImageConverter::variantForImage(QImage::fromData(pixmapData)); } args.append(hintsMap); // hints // Persistent => 0 == infinite timeout // CloseOnTimeout => -1 == let the server decide int timeout = (notification->flags() & KNotification::Persistent) ? 0 : -1; args.append(timeout); // expire timout dbusNotificationMessage.setArguments(args); QDBusPendingCall notificationCall = QDBusConnection::sessionBus().asyncCall(dbusNotificationMessage, -1); //parent is set to the notification so that no-one ever accesses a dangling pointer on the notificationObject property QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(notificationCall, notification); watcher->setProperty("notificationObject", QVariant::fromValue(notification)); QObject::connect(watcher, &QDBusPendingCallWatcher::finished, q, &NotifyByPopup::onGalagoServerReply); return true; } void NotifyByPopupPrivate::closeGalagoNotification(KNotification *notification) { uint galagoId = galagoNotifications.key(notification, 0); if (galagoId == 0) { qCDebug(LOG_KNOTIFICATIONS) << "not found dbus id to close" << notification->id(); return; } QDBusMessage m = QDBusMessage::createMethodCall(QString::fromLatin1(dbusServiceName), QString::fromLatin1(dbusPath), QString::fromLatin1(dbusInterfaceName), QStringLiteral("CloseNotification")); QList args; args.append(galagoId); m.setArguments(args); // send(..) does not block bool queued = QDBusConnection::sessionBus().send(m); if (!queued) { qCWarning(LOG_KNOTIFICATIONS) << "Failed to queue dbus message for closing a notification"; } } void NotifyByPopupPrivate::queryPopupServerCapabilities() { if (!dbusServiceExists) { if (NotifyByPopupGrowl::canPopup()) { popupServerCapabilities = NotifyByPopupGrowl::capabilities(); } else { // Return capabilities of the KPassivePopup implementation popupServerCapabilities = QStringList() << QStringLiteral("actions") << QStringLiteral("body") << QStringLiteral("body-hyperlinks") << QStringLiteral("body-markup") << QStringLiteral("icon-static"); } } if (dbusServiceCapCacheDirty) { QDBusMessage m = QDBusMessage::createMethodCall(QString::fromLatin1(dbusServiceName), QString::fromLatin1(dbusPath), QString::fromLatin1(dbusInterfaceName), QStringLiteral("GetCapabilities")); QDBusConnection::sessionBus().callWithCallback(m, q, SLOT(onGalagoServerCapabilitiesReceived(QStringList)), nullptr, -1); } } QString NotifyByPopupPrivate::stripHtml(const QString &text) { QXmlStreamReader r(QStringLiteral("") + text + QStringLiteral("")); HtmlEntityResolver resolver; r.setEntityResolver(&resolver); QString result; while (!r.atEnd()) { r.readNext(); if (r.tokenType() == QXmlStreamReader::Characters) { result.append(r.text()); } else if (r.tokenType() == QXmlStreamReader::StartElement && r.name() == QLatin1String("br")) { result.append(QLatin1Char('\n')); } } if (r.hasError()) { // XML error in the given text, just return the original string qCWarning(LOG_KNOTIFICATIONS) << "Notification to send to backend which does " "not support HTML, contains invalid XML:" << r.errorString() << "line" << r.lineNumber() << "col" << r.columnNumber(); return text; } return result; } QString NotifyByPopupPrivate::HtmlEntityResolver::resolveUndeclaredEntity(const QString &name) { QString result = QXmlStreamEntityResolver::resolveUndeclaredEntity(name); if (!result.isEmpty()) { return result; } QChar ent = KCharsets::fromEntity(QLatin1Char('&') + name); if (ent.isNull()) { qCWarning(LOG_KNOTIFICATIONS) << "Notification to send to backend which does " "not support HTML, contains invalid entity: " << name; ent = QLatin1Char(' '); } return QString(ent); }