diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e322f4b..ec7bb5f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,214 +1,213 @@ if (Phonon4Qt5_FOUND) include_directories(${PHONON_INCLUDE_DIR}) endif() if (CANBERRA_FOUND) include_directories(${CANBERRA_INCLUDE_DIRS}) endif() ecm_create_qm_loader(knotifications_QM_LOADER knotifications5_qt) set(knotifications_SRCS knotification.cpp knotificationmanager.cpp kpassivepopup.cpp knotifyconfig.cpp knotificationplugin.cpp - notifybypopupgrowl.cpp notifybyexecute.cpp notifybylogfile.cpp notifybytaskbar.cpp ${knotifications_QM_LOADER} ) if (TARGET Qt5::DBus) list(APPEND knotifications_SRCS kstatusnotifieritem.cpp kstatusnotifieritemdbus_p.cpp knotificationrestrictions.cpp imageconverter.cpp #needed to marshal images for sending over dbus by NotifyByPopup notifybypopup.cpp notifybyportal.cpp ) endif() if (ANDROID) add_subdirectory(android) list(APPEND knotifications_SRCS notifybyandroid.cpp knotifications.qrc) endif() if (WIN32) list(APPEND knotifications_SRCS notifybysnore.cpp) endif () if (APPLE) list(APPEND knotifications_SRCS notifybymacosnotificationcenter.mm) endif() ecm_qt_declare_logging_category(knotifications_SRCS HEADER debug_p.h IDENTIFIER LOG_KNOTIFICATIONS CATEGORY_NAME org.kde.knotifications) if (Canberra_FOUND) set(knotifications_SRCS ${knotifications_SRCS} notifybyaudio_canberra.cpp) elseif (Phonon4Qt5_FOUND) set(knotifications_SRCS ${knotifications_SRCS} notifybyaudio_phonon.cpp) endif() if (Qt5TextToSpeech_FOUND) set(knotifications_SRCS ${knotifications_SRCS} notifybytts.cpp) endif() if (TARGET Qt5::DBus) if (dbusmenu-qt5_FOUND) message("dbusmenu-qt5_FOUND") set(HAVE_DBUSMENUQT 1) include_directories(${dbusmenu-qt5_INCLUDE_DIRS}) else() set(HAVE_DBUSMENUQT 0) endif() qt5_add_dbus_adaptor(knotifications_SRCS org.kde.StatusNotifierItem.xml kstatusnotifieritemdbus_p.h KStatusNotifierItemDBus) set(statusnotifierwatcher_xml org.kde.StatusNotifierWatcher.xml) qt5_add_dbus_interface(knotifications_SRCS ${statusnotifierwatcher_xml} statusnotifierwatcher_interface) set(notifications_xml org.freedesktop.Notifications.xml) qt5_add_dbus_interface(knotifications_SRCS ${notifications_xml} notifications_interface) endif() configure_file(config-knotifications.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-knotifications.h ) add_library(KF5Notifications ${knotifications_SRCS}) generate_export_header(KF5Notifications BASE_NAME KNotifications) add_library(KF5::Notifications ALIAS KF5Notifications) target_include_directories(KF5Notifications INTERFACE "$") target_link_libraries(KF5Notifications PUBLIC Qt5::Widgets ) if (TARGET Qt5::DBus) target_link_libraries(KF5Notifications PUBLIC Qt5::DBus) endif() target_link_libraries(KF5Notifications PRIVATE KF5::CoreAddons KF5::ConfigCore KF5::WindowSystem KF5::Codecs ) if (TARGET SnoreToast::SnoreToastActions) target_link_libraries(KF5Notifications PRIVATE Qt5::Network SnoreToast::SnoreToastActions) endif () if (Phonon4Qt5_FOUND) target_link_libraries(KF5Notifications PRIVATE ${PHONON_LIBRARIES}) endif() if (Canberra_FOUND) target_link_libraries(KF5Notifications PRIVATE Canberra::Canberra) endif() if (Qt5TextToSpeech_FOUND) target_link_libraries(KF5Notifications PRIVATE Qt5::TextToSpeech) endif() if(X11_FOUND) target_link_libraries(KF5Notifications PRIVATE ${X11_X11_LIB} Qt5::X11Extras) endif() if(APPLE) target_link_libraries(KF5Notifications PRIVATE Qt5::MacExtras "-framework Foundation" "-framework AppKit") endif() if(X11_XTest_FOUND) target_link_libraries(KF5Notifications PRIVATE ${X11_XTest_LIB}) endif() if(HAVE_DBUSMENUQT) target_link_libraries(KF5Notifications PRIVATE dbusmenu-qt5) endif() if (ANDROID) target_link_libraries(KF5Notifications PRIVATE Qt5::AndroidExtras) endif() set_target_properties(KF5Notifications PROPERTIES VERSION ${KNOTIFICATIONS_VERSION_STRING} SOVERSION ${KNOTIFICATIONS_SOVERSION} EXPORT_NAME Notifications ) ecm_generate_headers(KNotifications_HEADERS HEADER_NAMES KNotification KPassivePopup KStatusNotifierItem KNotificationRestrictions KNotificationPlugin KNotifyConfig REQUIRED_HEADERS KNotifications_HEADERS ) install(TARGETS KF5Notifications EXPORT KF5NotificationsTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/knotifications_export.h ${KNotifications_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KNotifications COMPONENT Devel ) if(BUILD_QCH) ecm_add_qch( KF5Notifications_QCH NAME KNotifications BASE_NAME KF5Notifications VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KNotifications_HEADERS} MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics" LINK_QCHS Qt5Widgets_QCH INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR} BLANK_MACROS KNOTIFICATIONS_EXPORT KNOTIFICATIONS_DEPRECATED KNOTIFICATIONS_DEPRECATED_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() if (TARGET Qt5::DBus) install(FILES org.kde.StatusNotifierItem.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} RENAME kf5_org.kde.StatusNotifierItem.xml) install(FILES org.kde.StatusNotifierWatcher.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} RENAME kf5_org.kde.StatusNotifierWatcher.xml) endif() install(FILES knotificationplugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR}) include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KNotifications LIB_NAME KF5Notifications DEPS "widgets" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KNotifications) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) if (ANDROID) install(FILES KF5Notifications-android-dependencies.xml DESTINATION ${KDE_INSTALL_LIBDIR}) endif() diff --git a/src/notifybypopup.cpp b/src/notifybypopup.cpp index 3945388..3f150f2 100644 --- a/src/notifybypopup.cpp +++ b/src/notifybypopup.cpp @@ -1,873 +1,837 @@ /* 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 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 == QLatin1String("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(QLatin1String("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(QLatin1String("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; } 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") + // 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); } diff --git a/src/notifybypopupgrowl.cpp b/src/notifybypopupgrowl.cpp deleted file mode 100644 index a7c2c7e..0000000 --- a/src/notifybypopupgrowl.cpp +++ /dev/null @@ -1,66 +0,0 @@ -/* - Copyright (C) 2010 by Sjors Gielen - - 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 "notifybypopupgrowl.h" -#include -#include - -#define GROWL_LOCATION_MACOSX "/Library/PreferencePanes/Growl.prefPane/Contents/MacOS/Growl" -#define GROWL_LOCATION_WIN32 "C:/Program Files/Growl for Windows/Growl.exe" - -/** - * @brief Check if Growl can display plugins. - * Currently, this checks only if Growl is installed, not if it's running. - * As soon as the Growl Notification Protocol is finished, it will be - * implemented and used for this check. - */ -bool NotifyByPopupGrowl::canPopup() -{ - return QFile::exists(QStringLiteral(GROWL_LOCATION_MACOSX)) - || QFile::exists(QStringLiteral(GROWL_LOCATION_WIN32)); -} - -/** - * @brief Get the capabilities supported by Growl. - */ -QStringList NotifyByPopupGrowl::capabilities() -{ - return QStringList(); -} - -/** - * @brief Send a popup through Growl. - * @param icon The icon inside the notification. Currently ignored. - * @param timeout The time in ms to show the notification. - * @param title The title displayed inside the notification. - * @param message The message displayed inside the notification. - */ -void NotifyByPopupGrowl::popup(const QPixmap *icon, int timeout, - const QString &title, const QString &message ) -{ - Q_UNUSED(icon); - - QSystemTrayIcon i; - i.show(); - i.showMessage(title, message, - QSystemTrayIcon::Information, timeout); - i.hide(); -} diff --git a/src/notifybypopupgrowl.h b/src/notifybypopupgrowl.h deleted file mode 100644 index d47df8c..0000000 --- a/src/notifybypopupgrowl.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright (C) 2010 by Sjors Gielen - - 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 . - - */ - -#ifndef NOTIFYBYPOPUPGROWL_H -#define NOTIFYBYPOPUPGROWL_H - -#include -#include - -/** - * @brief Display a notification using Growl. - * - * Currently, this class uses QSystemTrayIcon to actually display the - * notification. Inside the Growl project, a protocol is being developed - * to display notifications; this protocol is currently only implemented - * (partly) in the Windows version of Growl. Once it is finished, it will be - * implemented in KNotify and used instead. - * (The normal Growl API is written in Objective C. It's possible to use it, - * but it's a lot harder than just waiting for GNTP to stabilize.) - */ -class NotifyByPopupGrowl -{ -public: - static bool canPopup(); - static QStringList capabilities(); - static void popup(const QPixmap *icon, int timeout, - const QString &title, const QString &message); -}; - -#endif // NOTIFYBYPOPUPGROWL_H