diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,17 @@ list(APPEND knotifications_SRCS notifybyandroid.cpp knotifications.qrc) endif() +if (WIN32) + find_package(LibSnoreToast REQUIRED) + set_package_properties(LibSnoreToast PROPERTIES TYPE REQUIRED + PURPOSE "Enable support for the Windows Toast Notifications with SnoreToast back-end for KNotifications" + DESCRIPTION "A command line application which is capable of creating Windows Toast notifications on Windows 8 or later." + ) + find_package(Qt5Core REQUIRED) + find_package(Qt5Network REQUIRED) + 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) @@ -97,6 +108,9 @@ KF5::WindowSystem KF5::Codecs ) +if (TARGET SnoreToast::SnoreToastActions) + target_link_libraries(KF5Notifications PRIVATE Qt5::Core 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 @@ -376,10 +376,12 @@ static QString defaultComponentName() { -#ifndef Q_OS_ANDROID - return QStringLiteral("plasma_workspace"); -#else +#if defined(Q_OS_ANDROID) return QStringLiteral("android_defaults"); +#elif defined(Q_OS_WIN) + return QStringLiteral("win32_defaults"); +#else + return QStringLiteral("plasma_workspace"); #endif } diff --git a/src/knotificationmanager.cpp b/src/knotificationmanager.cpp --- a/src/knotificationmanager.cpp +++ b/src/knotificationmanager.cpp @@ -40,12 +40,16 @@ #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" #if defined(HAVE_CANBERRA) @@ -91,10 +95,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")); } @@ -133,24 +137,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); 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: + QMap> m_notifications; + QString program = QStringLiteral("SnoreToast.exe"); + QLocalServer server; + QTemporaryDir 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,189 @@ +/* + 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 +#include +#include +#include +#include +#include + +#include + +/** + * Be sure to have a shortcut installed in Windows Start Menu by SnoreToast + * The syntax 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. + */ + +NotifyBySnore::NotifyBySnore(QObject* parent) : + KNotificationPlugin(parent) +{ + server.listen(QString::fromStdString(QCryptographicHash::hash(qApp->applicationDirPath().toUtf8(), \ + QCryptographicHash::Md5 ).toHex().toStdString()).left(5)); + // ^ can be increased from 5 to N for lesser collisions + + QObject::connect(&server, &QLocalServer::newConnection, &server, [this]() { + auto sock = server.nextPendingConnection(); + sock->waitForReadyRead(); + const QByteArray rawData = sock->readAll(); + sock->close(); + const QString data = + QString::fromWCharArray(reinterpret_cast(rawData.constData()), + rawData.size() / sizeof(wchar_t)); + QMap map; + for (const auto &str : data.split(QStringLiteral(";"))) { + const auto index = str.indexOf(QStringLiteral("=")); + map.insert(str.mid(0, index), str.mid(index + 1)); + } + const auto action = map[QStringLiteral("action")]; + const auto id = map[QStringLiteral("notificationId")].toInt(); + KNotification *notification = nullptr; + const auto it = m_notifications.find(id); + if (it != m_notifications.end()) { + notification = it.value(); + } + const auto snoreAction = SnoreToastActions::getAction(action.toStdWString()); + 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")]; + 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() +{ + server.close(); + iconDir.remove(); +} + +void NotifyBySnore::notify(KNotification *notification, KNotifyConfig *config) +{ + if (m_notifications.constFind(notification->id()) != m_notifications.constEnd()) { + qCDebug(LOG_KNOTIFICATIONS) << "Duplicate notification with ID: " << notification->id() << " ignored."; + return; + } + QProcess *proc = new QProcess(); + QStringList arguments; + QString iconPath; + + arguments << QStringLiteral("-t"); + if (!notification->title().isEmpty()) { + arguments << notification->title(); + } + else { + arguments << qApp->applicationDisplayName(); + } + arguments << QStringLiteral("-m") << notification->text(); + if (!notification->pixmap().isNull()) { + iconPath = iconDir.path() + QStringLiteral("/") + + 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") << server.fullServerName(); + + if (!notification->actions().isEmpty()) { + arguments << QStringLiteral("-b") << notification->actions().join(QStringLiteral(";")); + } + qCDebug(LOG_KNOTIFICATIONS) << arguments; + + m_notifications.insert(notification->id(), notification); + proc->start(program, arguments); + + connect(proc, QOverload::of(&QProcess::finished), + [=](int exitCode, QProcess::ExitStatus exitStatus){ + proc->deleteLater(); + }); +} + +void NotifyBySnore::close(KNotification *notification) +{ + const auto it = m_notifications.find(notification->id()); + if (it == m_notifications.end()) { + return; + } + + qCDebug(LOG_KNOTIFICATIONS) << "SnoreToast closing notification with ID: " << notification->id(); + + QProcess *proc = new QProcess(); + QStringList arguments; + arguments << QStringLiteral("-close") << QString::number(notification->id()) + << QStringLiteral("-appID") << qApp->applicationName(); + proc->start(program, arguments); + if (it.value()) { + finish(it.value()); + } + m_notifications.erase(it); + connect(proc, QOverload::of(&QProcess::finished), + [=](int exitCode, QProcess::ExitStatus exitStatus){ + proc->deleteLater(); + delete proc; + }); +} + +void NotifyBySnore::update(KNotification *notification, KNotifyConfig *config) +{ + close(notification); + notify(notification, config); +}