diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,26 +66,42 @@ if(APPLE) find_package(Qt5MacExtras ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) endif() +if (ANDROID) + find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED AndroidExtras) + find_package(Gradle REQUIRED) +endif() +if (WIN32) + find_package(LibSnoreToast REQUIRED) + set_package_properties(LibSnoreToast PROPERTIES TYPE REQUIRED + PURPOSE "for the Windows Toast Notifications" + DESCRIPTION "A command line application, capable of creating + Windows Toast notifications on Windows (>=)8 " + ) + find_package(Qt5Network REQUIRED) +endif() find_package(KF5WindowSystem ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Config ${KF5_DEP_VERSION} REQUIRED) find_package(KF5Codecs ${KF5_DEP_VERSION} REQUIRED) find_package(KF5CoreAddons ${KF5_DEP_VERSION} REQUIRED) -find_package(Canberra) -set_package_properties(Canberra PROPERTIES DESCRIPTION "Library for generating event sounds" - PURPOSE "Needed to build audio notification support" - URL "http://0pointer.de/lennart/projects/libcanberra" - TYPE OPTIONAL) -if (Canberra_FOUND) - add_definitions(-DHAVE_CANBERRA) -elseif(NOT ANDROID) - # This is REQUIRED since you cannot tell CMake "either one of those two optional ones are required" - find_package(Phonon4Qt5 4.6.60 NO_MODULE REQUIRED) - set_package_properties(Phonon4Qt5 PROPERTIES - DESCRIPTION "Qt-based audio library" - PURPOSE "Needed to build audio notification support when Canberra isn't available") +if (NOT WIN32) + find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED DBus) + find_package(Canberra) + set_package_properties(Canberra PROPERTIES DESCRIPTION "Library for generating event sounds" + PURPOSE "Needed to build audio notification support" + URL "http://0pointer.de/lennart/projects/libcanberra" + TYPE OPTIONAL) + if (Canberra_FOUND) + add_definitions(-DHAVE_CANBERRA) + else() + # This is REQUIRED since you cannot tell CMake "either one of those two optional ones are required" + find_package(Phonon4Qt5 4.6.60 NO_MODULE REQUIRED) + set_package_properties(Phonon4Qt5 PROPERTIES + DESCRIPTION "Qt-based audio library" + PURPOSE "Needed to build audio notification support when Canberra isn't available") add_definitions(-DHAVE_PHONON4QT5) + endif() endif() remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY) @@ -119,6 +135,11 @@ set(HAVE_DBUS FALSE) if (TARGET Qt5::DBus) + find_package(dbusmenu-qt5 CONFIG) + set_package_properties(dbusmenu-qt5 PROPERTIES DESCRIPTION "DBusMenuQt" + URL "https://launchpad.net/libdbusmenu-qt" TYPE OPTIONAL + PURPOSE "Support for notification area menus via the DBusMenu protocol") + set(HAVE_DBUS TRUE) endif() configure_package_config_file( diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,10 @@ list(APPEND knotifications_SRCS notifybyandroid.cpp knotifications.qrc) endif() +if (WIN32) + list(APPEND knotifications_SRCS notifybysnore.cpp) +endif () + ecm_qt_declare_logging_category(knotifications_SRCS HEADER debug_p.h IDENTIFIER LOG_KNOTIFICATIONS CATEGORY_NAME org.kde.knotifications) if (Canberra_FOUND) @@ -54,27 +58,25 @@ notifybytts.cpp) endif() -find_package(dbusmenu-qt5 CONFIG) -set_package_properties(dbusmenu-qt5 PROPERTIES DESCRIPTION "DBusMenuQt" - URL "https://launchpad.net/libdbusmenu-qt" TYPE OPTIONAL - PURPOSE "Support for notification area menus via the DBusMenu protocol") -if (dbusmenu-qt5_FOUND) - message("dbusmenu-qt5_FOUND") - set(HAVE_DBUSMENUQT 1) - include_directories(${dbusmenu-qt5_INCLUDE_DIRS}) -else() - set(HAVE_DBUSMENUQT 0) -endif() - if (TARGET Qt5::DBus) - qt5_add_dbus_adaptor(knotifications_SRCS org.kde.StatusNotifierItem.xml - kstatusnotifieritemdbus_p.h KStatusNotifierItemDBus) + if (dbusmenu-qt5_FOUND) + message("dbusmenu-qt5_FOUND") + set(HAVE_DBUSMENUQT 1) + include_directories(${dbusmenu-qt5_INCLUDE_DIRS}) + else() + set(HAVE_DBUSMENUQT 0) + endif() + + if (TARGET Qt5::DBus) + 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) + 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() endif() configure_file(config-knotifications.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-knotifications.h ) @@ -97,6 +99,9 @@ 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 diff --git a/src/knotification.cpp b/src/knotification.cpp --- a/src/knotification.cpp +++ b/src/knotification.cpp @@ -372,10 +372,10 @@ static QString defaultComponentName() { -#ifndef Q_OS_ANDROID - return QStringLiteral("plasma_workspace"); -#else +#if defined(Q_OS_ANDROID) return QStringLiteral("android_defaults"); +#else + return QStringLiteral("plasma_workspace"); #endif } @@ -573,3 +573,5 @@ return d->hints; } +} + diff --git a/src/knotificationmanager.cpp b/src/knotificationmanager.cpp --- a/src/knotificationmanager.cpp +++ b/src/knotificationmanager.cpp @@ -37,11 +37,14 @@ #include "notifybylogfile.h" #include "notifybytaskbar.h" #include "notifybyexecute.h" -#ifndef Q_OS_ANDROID + +#if defined(Q_OS_ANDROID) +#include "notifybyandroid.h" +#elif defined(Q_OS_WIN) +#include "notifybysnore.h" +#else #include "notifybypopup.h" #include "notifybyportal.h" -#else -#include "notifybyandroid.h" #endif #include "debug_p.h" @@ -88,10 +91,10 @@ d->notifyPlugins.clear(); #ifdef QT_DBUS_LIB - const bool inSandbox = QFileInfo::exists(QLatin1String("/.flatpak-info")) || qEnvironmentVariableIsSet("SNAP"); + const bool inSandbox = QFileInfo::exists(QLatin1String("/.flatpak-info")) || qEnvironmentVariableIsSet("SNAP"); - if (inSandbox) { - QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); + if (inSandbox) { + QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop")); } @@ -130,24 +133,25 @@ // We have a series of built-ins up first, and fall back to trying // to instantiate an externally supplied plugin. if (action == QLatin1String("Popup")) { -#ifndef Q_OS_ANDROID +#if defined(Q_OS_ANDROID) + plugin = new NotifyByAndroid(this); +#elif defined(Q_OS_WIN) + plugin = new NotifyBySnore(this); +#else if (d->portalDBusServiceExists) { plugin = new NotifyByPortal(this); } else { plugin = new NotifyByPopup(this); - } -#else - plugin = new NotifyByAndroid(this); + } #endif - addPlugin(plugin); } else if (action == QLatin1String("Taskbar")) { plugin = new NotifyByTaskbar(this); addPlugin(plugin); } else if (action == QLatin1String("Sound")) { #if defined(HAVE_PHONON4QT5) || defined(HAVE_CANBERRA) - plugin = new NotifyByAudio(this); - addPlugin(plugin); + plugin = new NotifyByAudio(this); + addPlugin(plugin); #endif } else if (action == QLatin1String("Execute")) { plugin = new NotifyByExecute(this); @@ -355,3 +359,5 @@ } #include "moc_knotificationmanager_p.cpp" + +#include "moc_knotificationmanager_p.cpp" diff --git a/src/knotifications.qrc b/src/knotifications.qrc --- a/src/knotifications.qrc +++ b/src/knotifications.qrc @@ -1,5 +1,6 @@ android_defaults.notifyrc + win32_defaults.notifyrc diff --git a/src/notifybysnore.h b/src/notifybysnore.h new file mode 100644 --- /dev/null +++ b/src/notifybysnore.h @@ -0,0 +1,49 @@ +/* + Copyright (C) 2019 Piyush Aggarwal + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of 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 General Public License + along with this program. If not, see . +*/ + +#ifndef NOTIFYBYSNORE_H +#define NOTIFYBYSNORE_H + +#include "knotificationplugin.h" + +#include +#include +#include +#include +#include + +/** Windows notification backend - inspired by Android notification backend. */ +class NotifyBySnore : public KNotificationPlugin +{ + Q_OBJECT + +public: + explicit NotifyBySnore(QObject *parent = nullptr); + ~NotifyBySnore() override; + + QString optionName() override { return QStringLiteral("Popup"); } + void notify(KNotification *notification, KNotifyConfig *config) override; + void close(KNotification * notification) override; + void update(KNotification *notification, KNotifyConfig *config) override; +private: + QHash> m_notifications; + QString m_program = QStringLiteral("SnoreToast.exe"); + QLocalServer m_server; + QTemporaryDir m_iconDir; +}; + +#endif // NOTIFYBYSNORE_H diff --git a/src/notifybysnore.cpp b/src/notifybysnore.cpp new file mode 100644 --- /dev/null +++ b/src/notifybysnore.cpp @@ -0,0 +1,201 @@ +/* + Copyright (C) 2019 Piyush Aggarwal + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of 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 General Public License + along with this program. If not, see . +*/ + +#include "notifybysnore.h" +#include "knotification.h" +#include "knotifyconfig.h" +#include "debug_p.h" + +#include +#include +#include +#include +#include + +#include + +/* + * On Windows a shortcut to your app is needed to be installed in the Start Menu + * (and subsequently, registered with the OS) in order to show notifications. + * Since KNotifications is a library, an app using it can't (feasibly) be properly + * registered with the OS. It is possible we could come up with some complicated solution + * which would require every KNotification-using app to do some special and probably + * difficult to understand change to support Windows. Or we can have SnoreToast.exe + * take care of all that nonsense for us. + * Note that, up to this point, there have been no special + * KNotifications changes to the generic application codebase to make this work, + * just some tweaks to the Craft blueprint and packaging script + * to pull in SnoreToast and trigger shortcut building respectively. + * Be sure to have a shortcut installed in Windows Start Menu by SnoreToast. + * + * So the location doesn't matter, but it's only possible to register the internal COM server in an executable. + * We could make it a static lib and link it in all KDE applications, + * but to make the action center integration work, we would need to also compile a class + * into the executable using a compile time uuid. + * + * The used header is meant to help with parsing the response. + * The cmake target for LibSnoreToast is a INTERFACE lib, it only provides the include path. + * + * + * Trigger the shortcut installation during the installation of your app; syntax for shortcut installation is - + * ./SnoreToast.exe -install + * + * appID: use as-is from your app's QCoreApplication::applicationName() when installing the shortcut. + * NOTE: Install the shortcut in Windows Start Menu folder. + * For example, check out Craft Blueprint for Quassel-IRC or KDE Connect. +*/ + +NotifyBySnore::NotifyBySnore(QObject* parent) : + KNotificationPlugin(parent) +{ + m_server.listen(QString::number(qHash(qApp->applicationDirPath()))); + connect(&m_server, &QLocalServer::newConnection, &m_server, [this]() { + auto sock = m_server.nextPendingConnection(); + sock->waitForReadyRead(); + const QByteArray rawData = sock->readAll(); + sock->deleteLater(); + const QString data = + QString::fromWCharArray(reinterpret_cast(rawData.constData()), + rawData.size() / sizeof(wchar_t)); + QMap map; + const auto parts = data.splitRef(QLatin1Char(';')); + for (auto &str : parts) { + const auto index = str.indexOf(QLatin1Char('=')); + map.insert(str.mid(0, index).toString(), str.mid(index + 1)); + } + const auto action = map[QStringLiteral("action")].toString(); + const auto id = map[QStringLiteral("notificationId")].toInt(); + KNotification *notification; + const auto it = m_notifications.constFind(id); + if (it != m_notifications.constEnd()) { + notification = it.value(); + } + else { + qCDebug(LOG_KNOTIFICATIONS) << "Notification not found!"; + return; + } + + // MSVC2019 has issues with QString::toStdWString() + // Qstring::toStdWString() doesn't work with MSVC2019 yet. If it gets fixed + // in future, feel free to change the implementation below for lesser LOC. + std::wstring waction(action.size(), 0); + action.toWCharArray(const_cast(waction.data())); + const auto snoreAction = SnoreToastActions::getAction(waction); + + qCDebug(LOG_KNOTIFICATIONS) << "The notification ID is : " << id; + switch (snoreAction) { + case SnoreToastActions::Actions::Clicked: + qCDebug(LOG_KNOTIFICATIONS) << " User clicked on the toast."; + if (notification) { + close(notification); + } + break; + case SnoreToastActions::Actions::Hidden: + qCDebug(LOG_KNOTIFICATIONS) << "The toast got hidden."; + break; + case SnoreToastActions::Actions::Dismissed: + qCDebug(LOG_KNOTIFICATIONS) << "User dismissed the toast."; + break; + case SnoreToastActions::Actions::Timedout: + qCDebug(LOG_KNOTIFICATIONS) << "The toast timed out."; + break; + case SnoreToastActions::Actions::ButtonClicked:{ + qCDebug(LOG_KNOTIFICATIONS) << " User clicked a button on the toast."; + const auto button = map[QStringLiteral("button")].toString(); + QStringList s = m_notifications.value(id)->actions(); + int actionNum = s.indexOf(button) + 1; // QStringList starts with index 0 but not actions + emit actionInvoked(id, actionNum); + break;} + case SnoreToastActions::Actions::TextEntered: + qCDebug(LOG_KNOTIFICATIONS) << " User entered some text in the toast."; + break; + default: + qCDebug(LOG_KNOTIFICATIONS) << "Unexpected behaviour with the toast."; + if (notification) { + close(notification); + } + break; + } + }); +} + +NotifyBySnore::~NotifyBySnore() +{ + m_server.close(); +} + +void NotifyBySnore::notify(KNotification *notification, KNotifyConfig *config) +{ + QProcess *proc = new QProcess(); + QStringList arguments; + + arguments << QStringLiteral("-t"); + if (!notification->title().isEmpty()) { + arguments << notification->title(); + } else { + arguments << qApp->applicationDisplayName(); + } + arguments << QStringLiteral("-m") << notification->text(); + if (!notification->pixmap().isNull()) { + auto iconPath = QString(m_iconDir.path() + QLatin1Char('/') + + QString::number(notification->id()) + QStringLiteral(".png")); + notification->pixmap().save(iconPath, "PNG"); + arguments << QStringLiteral("-p") << iconPath; + } + arguments << QStringLiteral("-appID") << qApp->applicationName() + << QStringLiteral("-id") << QString::number(notification->id()) + << QStringLiteral("-pipename") << m_server.fullServerName(); + + if (!notification->actions().isEmpty()) { + arguments << QStringLiteral("-b") << notification->actions().join(QLatin1Char(';')); + } + qCDebug(LOG_KNOTIFICATIONS) << arguments; + proc->start(m_program, arguments); + m_notifications.insert(notification->id(), notification); + connect(proc, QOverload::of(&QProcess::finished), + [=](int exitCode, QProcess::ExitStatus exitStatus){ + proc->deleteLater(); + if (exitStatus != QProcess::NormalExit) { + qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast crashed while trying to show a notification."; + close(notification); + } + QFile::remove(QString(m_iconDir.path() + QLatin1Char('/') + + QString::number(notification->id()) + QStringLiteral(".png"))); + }); +} + +void NotifyBySnore::close(KNotification *notification) +{ + if (m_notifications.constFind(notification->id()) == m_notifications.constEnd()) { + return; + } + qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast closing notification with ID: " << notification->id(); + QStringList arguments; + arguments << QStringLiteral("-close") << QString::number(notification->id()) + << QStringLiteral("-appID") << qApp->applicationName(); + QProcess::startDetached(m_program, arguments); + if (notification) { + finish(notification); + } + m_notifications.remove(notification->id()); +} + +void NotifyBySnore::update(KNotification *notification, KNotifyConfig *config) +{ + close(notification); + notify(notification, config); +}