diff --git a/CMakeLists.txt b/CMakeLists.txt index 35ce9cb..3758d24 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,125 +1,135 @@ cmake_minimum_required(VERSION 3.0) set(KF5_VERSION "5.49.0") # handled by release scripts set(KF5_DEP_VERSION "5.49.0") # handled by release scripts project(KNotifications VERSION ${KF5_VERSION}) # ECM setup include(FeatureSummary) find_package(ECM 5.49.0 NO_MODULE) set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules") feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) -set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) +set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules) include(GenerateExportHeader) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMQtDeclareLoggingCategory) include(ECMPoQmTools) include(ECMAddQch) option(BUILD_QCH "Build API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)" OFF) add_feature_info(QCH ${BUILD_QCH} "API documentation in QCH format (for e.g. Qt Assistant, Qt Creator & KDevelop)") ecm_setup_version(PROJECT VARIABLE_PREFIX KNOTIFICATIONS VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/knotifications_version.h" PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5NotificationsConfigVersion.cmake" SOVERSION 5) # Dependencies set(REQUIRED_QT_VERSION 5.8.0) find_package(Qt5 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets DBus) find_package(Qt5 ${REQUIRED_QT_VERSION} QUIET OPTIONAL_COMPONENTS TextToSpeech) set_package_properties(Qt5TextToSpeech PROPERTIES DESCRIPTION "Qt text to speech module" TYPE OPTIONAL PURPOSE "Required to build text to speech notification support") if (Qt5TextToSpeech_FOUND) add_definitions(-DHAVE_SPEECH) endif() include(KDEInstallDirs) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(KDECMakeSettings) if (NOT APPLE AND NOT WIN32) find_package(X11) endif() set(HAVE_X11 ${X11_FOUND}) set(HAVE_XTEST ${X11_XTest_FOUND}) if(X11_FOUND) find_package(Qt5X11Extras ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) endif() if(APPLE) find_package(Qt5MacExtras ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE) 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(Phonon4Qt5 4.6.60 REQUIRED NO_MODULE) -set_package_properties(Phonon4Qt5 PROPERTIES - DESCRIPTION "Qt-based audio library" - TYPE REQUIRED - PURPOSE "Required to build audio notification support") -if (Phonon4Qt5_FOUND) - add_definitions(-DHAVE_PHONON4QT5) +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() + find_package(Phonon4Qt5 4.6.60 NO_MODULE) + set_package_properties(Phonon4Qt5 PROPERTIES + DESCRIPTION "Qt-based audio library" + # This is REQUIRED since you cannot tell CMake "either one of those two optional ones are required" + TYPE REQUIRED + PURPOSE "Needed to build audio notification support when Canberra isn't available") + if (Phonon4Qt5_FOUND) + add_definitions(-DHAVE_PHONON4QT5) + endif() endif() remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY) if (IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/po") ecm_install_po_files_as_qm(po) endif() add_subdirectory(src) if (BUILD_TESTING) add_subdirectory(tests) add_subdirectory(autotests) endif() # create a Config.cmake and a ConfigVersion.cmake file and install them set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Notifications") if (BUILD_QCH) ecm_install_qch_export( TARGETS KF5Notifications_QCH FILE KF5NotificationsQchTargets.cmake DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) set(PACKAGE_INCLUDE_QCHTARGETS "include(\"\${CMAKE_CURRENT_LIST_DIR}/KF5NotificationsQchTargets.cmake\")") endif() include(CMakePackageConfigHelpers) configure_package_config_file( "${CMAKE_CURRENT_SOURCE_DIR}/KF5NotificationsConfig.cmake.in" "${CMAKE_CURRENT_BINARY_DIR}/KF5NotificationsConfig.cmake" PATH_VARS KDE_INSTALL_DBUSINTERFACEDIR INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR} ) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/KF5NotificationsConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/KF5NotificationsConfigVersion.cmake" DESTINATION "${CMAKECONFIG_INSTALL_DIR}" COMPONENT Devel ) install(EXPORT KF5NotificationsTargets DESTINATION "${CMAKECONFIG_INSTALL_DIR}" FILE KF5NotificationsTargets.cmake NAMESPACE KF5:: ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/knotifications_version.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5} COMPONENT Devel ) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/cmake/modules/FindCanberra.cmake b/cmake/modules/FindCanberra.cmake new file mode 100644 index 0000000..48e2d54 --- /dev/null +++ b/cmake/modules/FindCanberra.cmake @@ -0,0 +1,50 @@ +# - Find libcanberra's libraries and headers. +# This module defines the following variables: +# +# CANBERRA_FOUND - true if libcanberra was found +# CANBERRA_LIBRARIES - libcanberra libraries to link against +# CANBERRA_INCLUDE_DIRS - include path for libcanberra +# +# Copyright (c) 2012 Raphael Kubo da Costa +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the University nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. + +find_package(PkgConfig) +pkg_check_modules(PC_CANBERRA libcanberra) + +find_library(CANBERRA_LIBRARIES + NAMES canberra + HINTS ${PC_CANBERRA_LIBRARY_DIRS} ${PC_CANBERRA_LIBDIR} +) + +find_path(CANBERRA_INCLUDE_DIRS + NAMES canberra.h + HINTS ${PC_CANBERRA_INCLUDE_DIRS} ${PC_CANBERRA_INCLUDEDIR} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Canberra REQUIRED_VARS CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS) + +mark_as_advanced(CANBERRA_LIBRARIES CANBERRA_INCLUDE_DIRS) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f406d08..474d082 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,170 +1,181 @@ 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 kstatusnotifieritem.cpp kstatusnotifieritemdbus_p.cpp knotificationrestrictions.cpp knotifyconfig.cpp knotificationplugin.cpp notifybypopup.cpp imageconverter.cpp #needed to marshal images for sending over dbus by NotifyByPopup notifybypopupgrowl.cpp notifybyexecute.cpp notifybyflatpak.cpp notifybylogfile.cpp notifybytaskbar.cpp ${knotifications_QM_LOADER} ) ecm_qt_declare_logging_category(knotifications_SRCS HEADER debug_p.h IDENTIFIER LOG_KNOTIFICATIONS CATEGORY_NAME org.kde.knotifications) -if (Phonon4Qt5_FOUND) +if (CANBERRA_FOUND) + set(knotifications_SRCS ${knotifications_SRCS} + notifybyaudio_canberra.cpp) +elseif (Phonon4Qt5_FOUND) set(knotifications_SRCS ${knotifications_SRCS} - notifybyaudio.cpp) + notifybyaudio_phonon.cpp) endif() if (Qt5TextToSpeech_FOUND) set(knotifications_SRCS ${knotifications_SRCS} 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() 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) 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 Qt5::DBus ) target_link_libraries(KF5Notifications PRIVATE KF5::CoreAddons KF5::ConfigCore KF5::WindowSystem KF5::Codecs ) if (Phonon4Qt5_FOUND) target_link_libraries(KF5Notifications PRIVATE ${PHONON_LIBRARIES}) endif() +if (CANBERRA_FOUND) + target_link_libraries(KF5Notifications PRIVATE + ${CANBERRA_LIBRARIES}) +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) 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() 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 BLANK_MACROS KNOTIFICATIONS_EXPORT KNOTIFICATIONS_DEPRECATED KNOTIFICATIONS_DEPRECATED_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() 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) 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}) diff --git a/src/knotificationmanager.cpp b/src/knotificationmanager.cpp index dce2326..71eda6f 100644 --- a/src/knotificationmanager.cpp +++ b/src/knotificationmanager.cpp @@ -1,333 +1,335 @@ /* This file is part of the KDE libraries Copyright (C) 2005 Olivier Goffart Copyright (C) 2013-2015 Martin Klapetek Copyright (C) 2017 Eike Hein This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. 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 "knotificationmanager_p.h" #include "knotification.h" #include #include #include #include #include #include #include #include #include #include "knotifyconfig.h" #include "knotificationplugin.h" #include "notifybypopup.h" #include "notifybylogfile.h" #include "notifybytaskbar.h" #include "notifybyexecute.h" #include "notifybyflatpak.h" #include "debug_p.h" -#ifdef HAVE_PHONON4QT5 -#include "notifybyaudio.h" +#if defined(HAVE_CANBERRA) +#include "notifybyaudio_canberra.h" +#elif defined(HAVE_PHONON4QT5) +#include "notifybyaudio_phonon.h" #endif #ifdef HAVE_SPEECH #include "notifybytts.h" #endif typedef QHash Dict; struct Q_DECL_HIDDEN KNotificationManager::Private { QHash notifications; QHash notifyPlugins; // incremental ids for notifications int notifyIdCounter; QStringList dirtyConfigCache; bool inSandbox = false; bool portalDBusServiceExists = false; }; class KNotificationManagerSingleton { public: KNotificationManager instance; }; Q_GLOBAL_STATIC(KNotificationManagerSingleton, s_self) KNotificationManager *KNotificationManager::self() { return &s_self()->instance; } KNotificationManager::KNotificationManager() : d(new Private) { d->notifyIdCounter = 0; qDeleteAll(d->notifyPlugins); d->notifyPlugins.clear(); if (!qEnvironmentVariableIsEmpty("XDG_RUNTIME_DIR")) { const QByteArray runtimeDir = qgetenv("XDG_RUNTIME_DIR"); if (!runtimeDir.isEmpty()) { d->inSandbox = QFileInfo::exists(QFile::decodeName(runtimeDir) + QLatin1String("/flatpak-info")); } } if (d->inSandbox) { QDBusConnectionInterface *interface = QDBusConnection::sessionBus().interface(); d->portalDBusServiceExists = interface->isServiceRegistered(QStringLiteral("org.freedesktop.portal.Desktop")); } QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/Config"), QStringLiteral("org.kde.knotification"), QStringLiteral("reparseConfiguration"), this, SLOT(reparseConfiguration(QString))); } KNotificationManager::~KNotificationManager() { delete d; } KNotificationPlugin *KNotificationManager::pluginForAction(const QString &action) { KNotificationPlugin *plugin = d->notifyPlugins.value(action); // We already loaded a plugin for this action. if (plugin) { return plugin; } auto addPlugin = [this](KNotificationPlugin *plugin) { d->notifyPlugins[plugin->optionName()] = plugin; connect(plugin, SIGNAL(finished(KNotification*)), this, SLOT(notifyPluginFinished(KNotification*))); connect(plugin, SIGNAL(actionInvoked(int, int)), this, SLOT(notificationActivated(int, int))); }; // Load plugin. // We have a series of built-ins up first, and fall back to trying // to instanciate an externally supplied plugin. if (action == QLatin1String("Popup")) { if (d->inSandbox && d->portalDBusServiceExists) { plugin = new NotifyByFlatpak(this); } else { plugin = new NotifyByPopup(this); } addPlugin(plugin); } else if (action == QLatin1String("Taskbar")) { plugin = new NotifyByTaskbar(this); addPlugin(plugin); } else if (action == QLatin1String("Sound")) { -#ifdef HAVE_PHONON4QT5 +#if defined(HAVE_PHONON4QT5) || defined(HAVE_CANBERRA) plugin = new NotifyByAudio(this); addPlugin(plugin); #endif } else if (action == QLatin1String("Execute")) { plugin = new NotifyByExecute(this); addPlugin(plugin); } else if (action == QLatin1String("Logfile")) { plugin = new NotifyByLogfile(this); addPlugin(plugin); } else if (action == QLatin1String("TTS")) { #ifdef HAVE_SPEECH plugin = new NotifyByTTS(this); addPlugin(plugin); #endif } else { bool pluginFound = false; QList plugins = KPluginLoader::instantiatePlugins(QStringLiteral("knotification/notifyplugins"), [&action, &pluginFound](const KPluginMetaData &data) { // KPluginLoader::instantiatePlugins loops over the plugins it // found and calls this function to determine whether to // instantiate them. We use a `pluginFound` var outside the // lambda to break out of the loop once we got a match. // The reason we can't just use KPluginLoader::findPlugins, // loop over the meta data and instantiate only one plugin // is because the X-KDE-KNotification-OptionName field is // optional (see TODO note below) and the matching plugin // may be among the plugins which don't have it. if (pluginFound) { return false; } const QJsonObject &rawData = data.rawData(); // This field is new-ish and optional. If it's not set we always // instantiate the plugin, unless we already got a match. // TODO KF6: Require X-KDE-KNotification-OptionName be set and // reject plugins without it. if (rawData.contains(QStringLiteral("X-KDE-KNotification-OptionName"))) { if (rawData.value(QStringLiteral("X-KDE-KNotification-OptionName")) == action) { pluginFound = true; } else { return false; } } return true; }, this); Q_FOREACH (QObject *pluginObj, plugins) { KNotificationPlugin *notifyPlugin = qobject_cast(pluginObj); if (notifyPlugin) { // We try to avoid unnecessary instantiations (see above), but // when they happen keep the resulting plugins around. addPlugin(notifyPlugin); // Get ready to return the plugin we got asked for. if (notifyPlugin->optionName() == action) { plugin = notifyPlugin; } } else { // Not our/valid plugin, so delete the created object. pluginObj->deleteLater(); } } } return plugin; } void KNotificationManager::notifyPluginFinished(KNotification *notification) { if (!notification || !d->notifications.contains(notification->id())) { return; } notification->deref(); } void KNotificationManager::notificationActivated(int id, int action) { if (d->notifications.contains(id)) { qCDebug(LOG_KNOTIFICATIONS) << id << " " << action; KNotification *n = d->notifications[id]; n->activate(action); close(id); } } void KNotificationManager::notificationClosed() { KNotification *notification = qobject_cast(sender()); if (!notification) { return; } // We cannot do d->notifications.find(notification->id()); here because the // notification->id() is -1 or -2 at this point, so we need to look for value for (auto iter = d->notifications.begin(); iter != d->notifications.end(); ++iter) { if (iter.value() == notification) { d->notifications.erase(iter); break; } } } void KNotificationManager::close(int id, bool force) { if (force || d->notifications.contains(id)) { KNotification *n = d->notifications.value(id); qCDebug(LOG_KNOTIFICATIONS) << "Closing notification" << id; // Find plugins that are actually acting on this notification // call close() only on those, otherwise each KNotificationPlugin::close() // will call finish() which may close-and-delete the KNotification object // before it finishes calling close on all the other plugins. // For example: Action=Popup is a single actions but there is 5 loaded // plugins, calling close() on the second would already close-and-delete // the notification KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId()); QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action")); Q_FOREACH (const QString &action, notifyActions.split(QLatin1Char('|'))) { if (!d->notifyPlugins.contains(action)) { qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action; continue; } d->notifyPlugins[action]->close(n); } } } int KNotificationManager::notify(KNotification *n) { KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId()); if (d->dirtyConfigCache.contains(n->appName())) { notifyConfig.reparseSingleConfiguration(n->appName()); d->dirtyConfigCache.removeOne(n->appName()); } const QString notifyActions = notifyConfig.readEntry(QStringLiteral("Action")); if (notifyActions.isEmpty() || notifyActions == QLatin1String("None")) { // this will cause KNotification closing itself fast n->ref(); n->deref(); return -1; } d->notifications.insert(d->notifyIdCounter, n); Q_FOREACH (const QString &action, notifyActions.split(QLatin1Char('|'))) { KNotificationPlugin *notifyPlugin = pluginForAction(action); if (!notifyPlugin) { qCDebug(LOG_KNOTIFICATIONS) << "No plugin for action" << action; continue; } n->ref(); qCDebug(LOG_KNOTIFICATIONS) << "Calling notify on" << notifyPlugin->optionName(); notifyPlugin->notify(n, ¬ifyConfig); } connect(n, &KNotification::closed, this, &KNotificationManager::notificationClosed); return d->notifyIdCounter++; } void KNotificationManager::update(KNotification *n) { KNotifyConfig notifyConfig(n->appName(), n->contexts(), n->eventId()); Q_FOREACH (KNotificationPlugin *p, d->notifyPlugins) { p->update(n, ¬ifyConfig); } } void KNotificationManager::reemit(KNotification *n) { notify(n); } void KNotificationManager::reparseConfiguration(const QString &app) { if (!d->dirtyConfigCache.contains(app)) { d->dirtyConfigCache << app; } } #include "moc_knotificationmanager_p.cpp" diff --git a/src/notifybyaudio_canberra.cpp b/src/notifybyaudio_canberra.cpp new file mode 100644 index 0000000..c8e0051 --- /dev/null +++ b/src/notifybyaudio_canberra.cpp @@ -0,0 +1,190 @@ +/* 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; + } + + ret = ca_context_change_props(m_context, + CA_PROP_APPLICATION_NAME, qUtf8Printable(qApp->applicationDisplayName()), + CA_PROP_APPLICATION_ID, qUtf8Printable(qApp->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 (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; + } + } +} + + +void NotifyByAudio::finishNotification(KNotification *notification, quint32 id) +{ + m_notifications.remove(id); + m_loopSoundUrls.remove(id); + finish(notification); +} diff --git a/src/notifybyaudio.h b/src/notifybyaudio_canberra.h similarity index 68% copy from src/notifybyaudio.h copy to src/notifybyaudio_canberra.h index bb2d749..2c9819b 100644 --- a/src/notifybyaudio.h +++ b/src/notifybyaudio_canberra.h @@ -1,64 +1,68 @@ /* This file is part of the KDE libraries Copyright 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 . */ #ifndef NOTIFYBYAUDIO_H #define NOTIFYBYAUDIO_H #include "knotificationplugin.h" -#include - -namespace Phonon { -// class MediaObject; -class MediaSource; -class AudioOutput; -} +#include +#include class KNotification; +struct ca_context; + class NotifyByAudio : public KNotificationPlugin { Q_OBJECT public: explicit NotifyByAudio(QObject *parent = nullptr); ~NotifyByAudio() override; QString optionName() override { return QStringLiteral("Sound"); } void notify(KNotification *notification, KNotifyConfig *config) override; void close(KNotification *notification) override; private Q_SLOTS: - void onAudioFinished(); - void onAudioSourceChanged(const Phonon::MediaSource &source); - void stateChanged(Phonon::State newState, Phonon::State oldState); - + void finishCallback(uint32_t id, + int error_code); private: - void finishNotification(KNotification *notification, Phonon::MediaObject *m); + static void ca_finish_callback(ca_context *c, + uint32_t id, + int error_code, + void *userdata); + + void finishNotification(KNotification *notification, quint32 id); + + bool playSound(quint32 id, const QUrl &url); - QList m_reusablePhonons; - QHash m_notifications; - Phonon::AudioOutput *m_audioOutput; + ca_context *m_context = nullptr; + quint32 m_currentId = 0; + QHash m_notifications; + // in case we loop we store the URL for the notification to be able to replay it + QHash m_loopSoundUrls; }; #endif diff --git a/src/notifybyaudio.cpp b/src/notifybyaudio_phonon.cpp similarity index 99% rename from src/notifybyaudio.cpp rename to src/notifybyaudio_phonon.cpp index 092be47..ae3c97a 100644 --- a/src/notifybyaudio.cpp +++ b/src/notifybyaudio_phonon.cpp @@ -1,188 +1,188 @@ /* This file is part of the KDE libraries Copyright (C) 2014-2015 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 "notifybyaudio.h" +#include "notifybyaudio_phonon.h" #include "debug_p.h" #include #include #include #include #include #include "knotifyconfig.h" #include "knotification.h" #include #include #include NotifyByAudio::NotifyByAudio(QObject *parent) : KNotificationPlugin(parent), m_audioOutput(nullptr) { } NotifyByAudio::~NotifyByAudio() { qDeleteAll(m_reusablePhonons); delete m_audioOutput; } void NotifyByAudio::notify(KNotification *notification, KNotifyConfig *config) { if (!m_audioOutput) { m_audioOutput = new Phonon::AudioOutput(Phonon::NotificationCategory, this); } 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() && QFile::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; } Phonon::MediaObject *m; if (m_reusablePhonons.isEmpty()) { m = new Phonon::MediaObject(this); connect(m, &Phonon::MediaObject::finished, this, &NotifyByAudio::onAudioFinished); connect(m, &Phonon::MediaObject::stateChanged, this, &NotifyByAudio::stateChanged); Phonon::createPath(m, m_audioOutput); } else { m = m_reusablePhonons.takeFirst(); } m->setCurrentSource(soundURL); m->play(); if (notification->flags() & KNotification::LoopSound) { // Enqueing essentially prevents the subsystem pipeline from partial teardown // which is the most desired thing in terms of load and delay between loop cycles. // All of this is timing dependent, which is why we want at least one source queued; // in reality the shorter the source the more sources we want to be queued to prevent // the MO from running out of sources. // Point being that all phonon signals are forcefully queued (becuase qthread has problems detecting !pthread threads), // so when you get for example the aboutToFinish signal the MO might already have stopped playing. // // And so we queue it three times at least; doesn't cost anything and keeps us safe. m->enqueue(soundURL); m->enqueue(soundURL); m->enqueue(soundURL); connect(m, SIGNAL(currentSourceChanged(Phonon::MediaSource)), SLOT(onAudioSourceChanged(Phonon::MediaSource))); } Q_ASSERT(!m_notifications.value(m)); m_notifications.insert(m, notification); } void NotifyByAudio::stateChanged(Phonon::State newState, Phonon::State oldState) { qCDebug(LOG_KNOTIFICATIONS) << "Changing audio state from" << oldState << "to" << newState; } void NotifyByAudio::close(KNotification *notification) { Phonon::MediaObject *m = m_notifications.key(notification); if (!m) { return; } m->stop(); finishNotification(notification, m); } void NotifyByAudio::onAudioFinished() { Phonon::MediaObject *m = qobject_cast(sender()); if (!m) { return; } KNotification *notification = m_notifications.value(m, nullptr); if (!notification) { // This means that close was called already so there's nothing else to do. // Ideally we should not be getting here if close has already been called // since stoping a mediaobject means it won't emit finished() *BUT* // since the finished signal is a queued connection in phonon it can happen // that the playing had already finished and we just had not got the signal yet return; } //if the sound is short enough, we can't guarantee new sounds are //enqueued before finished is emitted. //so to make sure we are looping restart it when the sound finished if (notification && (notification->flags() & KNotification::LoopSound)) { m->play(); return; } finishNotification(notification, m); } void NotifyByAudio::finishNotification(KNotification *notification, Phonon::MediaObject *m) { m_notifications.remove(m); if (notification) { finish(notification); } disconnect(m, SIGNAL(currentSourceChanged(Phonon::MediaSource)), this, SLOT(onAudioSourceChanged(Phonon::MediaSource))); m_reusablePhonons.append(m); } void NotifyByAudio::onAudioSourceChanged(const Phonon::MediaSource &source) { Phonon::MediaObject *m = qobject_cast(sender()); if (!m) { return; } m->enqueue(source); } diff --git a/src/notifybyaudio.h b/src/notifybyaudio_phonon.h similarity index 100% rename from src/notifybyaudio.h rename to src/notifybyaudio_phonon.h