diff --git a/CMakeLists.txt b/CMakeLists.txt index e3653fc5fc..3e6464860c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,6 +229,8 @@ else() set(HAVE_XKBCOMMON_NO_SECURE_GETENV 0) endif() +find_package(Canberra REQUIRED) + if (KWIN_BUILD_X11) pkg_check_modules(XKBX11 IMPORTED_TARGET xkbcommon-x11 REQUIRED) add_feature_info(XKBX11 XKBX11_FOUND "Required for handling keyboard events in X11 backend") diff --git a/src/effect/effect.h b/src/effect/effect.h index 7ff1202d55..d0a5e1b778 100644 --- a/src/effect/effect.h +++ b/src/effect/effect.h @@ -546,7 +546,8 @@ public: ScreenInversion, Blur, Contrast, - HighlightWindows + HighlightWindows, + SystemBell, }; /** diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index 79b0e22101..b63174d51e 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -101,6 +101,7 @@ add_subdirectory(squash) add_subdirectory(startupfeedback) add_subdirectory(stickykeys) add_subdirectory(synchronizeskipswitcher) +add_subdirectory(systembell) add_subdirectory(thumbnailaside) add_subdirectory(tileseditor) add_subdirectory(touchpoints) diff --git a/src/plugins/systembell/CMakeLists.txt b/src/plugins/systembell/CMakeLists.txt new file mode 100644 index 0000000000..187d9e536a --- /dev/null +++ b/src/plugins/systembell/CMakeLists.txt @@ -0,0 +1,14 @@ +set(systembell_SOURCES + systembell.cpp + systembell.qrc + main.cpp +) + +kwin_add_builtin_effect(systembell ${systembell_SOURCES}) +target_link_libraries(systembell PRIVATE + kwin + + KF6::GlobalAccel + KF6::I18n + Canberra::Canberra +) diff --git a/src/plugins/systembell/main.cpp b/src/plugins/systembell/main.cpp new file mode 100644 index 0000000000..58cbbfb900 --- /dev/null +++ b/src/plugins/systembell/main.cpp @@ -0,0 +1,18 @@ +/* + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systembell.h" + +namespace KWin +{ + +KWIN_EFFECT_FACTORY_SUPPORTED(SystemBellEffect, + "metadata.json.stripped", + return SystemBellEffect::supported();) + +} // namespace KWin + +#include "main.moc" diff --git a/src/plugins/systembell/metadata.json b/src/plugins/systembell/metadata.json new file mode 100644 index 0000000000..5b91c62df6 --- /dev/null +++ b/src/plugins/systembell/metadata.json @@ -0,0 +1,12 @@ +{ + "KPlugin": { + "Category": "Accessibility", + "Description": "Provides the bell", + "EnabledByDefault": true, + "License": "GPL", + "Name": "System Bell" + }, + "org.kde.kwin.effect": { + "internal": true + } +} diff --git a/src/plugins/systembell/shaders/color.frag b/src/plugins/systembell/shaders/color.frag new file mode 100644 index 0000000000..54664a157e --- /dev/null +++ b/src/plugins/systembell/shaders/color.frag @@ -0,0 +1,6 @@ +uniform vec4 geometryColor; + +void main() +{ + gl_FragColor = geometryColor; +} diff --git a/src/plugins/systembell/shaders/color_core.frag b/src/plugins/systembell/shaders/color_core.frag new file mode 100644 index 0000000000..54664a157e --- /dev/null +++ b/src/plugins/systembell/shaders/color_core.frag @@ -0,0 +1,6 @@ +uniform vec4 geometryColor; + +void main() +{ + gl_FragColor = geometryColor; +} diff --git a/src/plugins/systembell/shaders/invert.frag b/src/plugins/systembell/shaders/invert.frag new file mode 100644 index 0000000000..539adc3083 --- /dev/null +++ b/src/plugins/systembell/shaders/invert.frag @@ -0,0 +1,24 @@ +#include "colormanagement.glsl" +#include "saturation.glsl" + +uniform sampler2D sampler; +uniform vec4 modulation; + +varying vec2 texcoord0; + +void main() +{ + vec4 tex = texture2D(sampler, texcoord0); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + tex = adjustSaturation(tex); + + // to preserve perceptual contrast, apply the inversion in gamma 2.2 space + tex = nitsToEncoding(tex, gamma22_EOTF, destinationReferenceLuminance); + tex.rgb /= max(0.001, tex.a); + tex.rgb = vec3(1.0) - tex.rgb; + tex *= modulation; + tex.rgb *= tex.a; + tex = encodingToNits(tex, gamma22_EOTF, destinationReferenceLuminance); + + gl_FragColor = nitsToDestinationEncoding(tex); +} diff --git a/src/plugins/systembell/shaders/invert_core.frag b/src/plugins/systembell/shaders/invert_core.frag new file mode 100644 index 0000000000..05c25dffa9 --- /dev/null +++ b/src/plugins/systembell/shaders/invert_core.frag @@ -0,0 +1,28 @@ +#version 140 + +#include "colormanagement.glsl" +#include "saturation.glsl" + +uniform sampler2D sampler; +uniform vec4 modulation; + +in vec2 texcoord0; + +out vec4 fragColor; + +void main() +{ + vec4 tex = texture(sampler, texcoord0); + tex = sourceEncodingToNitsInDestinationColorspace(tex); + tex = adjustSaturation(tex); + + // to preserve perceptual contrast, apply the inversion in gamma 2.2 space + tex = nitsToEncoding(tex, gamma22_EOTF, destinationReferenceLuminance); + tex.rgb /= max(0.001, tex.a); + tex.rgb = vec3(1.0) - tex.rgb; + tex *= modulation; + tex.rgb *= tex.a; + tex = encodingToNits(tex, gamma22_EOTF, destinationReferenceLuminance); + + fragColor = nitsToDestinationEncoding(tex); +} diff --git a/src/plugins/systembell/systembell.cpp b/src/plugins/systembell/systembell.cpp new file mode 100644 index 0000000000..6dc4b0483f --- /dev/null +++ b/src/plugins/systembell/systembell.cpp @@ -0,0 +1,241 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "systembell.h" + +#include "effect/effecthandler.h" +#include "opengl/glshader.h" +#include "opengl/glshadermanager.h" + +#include +#include +#include +#include + +#include + +Q_LOGGING_CATEGORY(KWIN_SYSTEMBELL, "kwin_effect_systembell", QtWarningMsg) + +static void ensureResources() +{ + // Must initialize resources manually because the effect is a static lib. + Q_INIT_RESOURCE(systembell); +} + +namespace KWin +{ + +SystemBellEffect::SystemBellEffect() + : m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kaccessrc"))) + , m_kdeglobals(QStringLiteral("kdeglobals")) +{ + QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/KWin/Effect/SystemBell1"), + QStringLiteral("org.kde.KWin.Effect.SystemBell1"), + this, + QDBusConnection::ExportAllSlots); + + connect(effects, &EffectsHandler::windowClosed, this, &SystemBellEffect::slotWindowClosed); + + const QLatin1String groupName("Bell"); + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) { + if (group.name() == groupName) { + m_bellConfig = group; + reconfigure(ReconfigureAll); + } + }); + m_bellConfig = m_configWatcher->config()->group(groupName); + reconfigure(ReconfigureAll); + + int ret = ca_context_create(&m_caContext); + if (ret != CA_SUCCESS) { + qCWarning(KWIN_SYSTEMBELL) << "Failed to initialize canberra context for audio notification:" << ca_strerror(ret); + m_caContext = nullptr; + } else { + ret = ca_context_change_props(m_caContext, + CA_PROP_APPLICATION_NAME, + qApp->applicationDisplayName().toUtf8().constData(), + CA_PROP_APPLICATION_ID, + qApp->desktopFileName().toUtf8().constData(), + nullptr); + if (ret != CA_SUCCESS) { + qCWarning(KWIN_SYSTEMBELL) << "Failed to set application properties on canberra context for audio notification:" << ca_strerror(ret); + } + } +} + +SystemBellEffect::~SystemBellEffect() +{ + if (m_caContext) { + ca_context_destroy(m_caContext); + } +} + +void SystemBellEffect::reconfigure(ReconfigureFlags flags) +{ + m_inited = false; + m_color = m_bellConfig.readEntry("VisibleBellColor", QColor(Qt::red)); + m_mode = m_bellConfig.readEntry("VisibleBellInvert", false) ? Invert : Color; + m_duration = m_bellConfig.readEntry("VisibleBellPause", 500); + m_audibleBell = m_bellConfig.readEntry("SystemBell", true); + m_customBell = m_bellConfig.readEntry("ArtsBell", false); + m_customBellFile = m_bellConfig.readEntry("ArtsBellFile", QString()); + m_visibleBell = m_bellConfig.readEntry("VisibleBell", false); +} + +bool SystemBellEffect::supported() +{ + return effects->compositingType() == OpenGLCompositing; +} + +void SystemBellEffect::flash(EffectWindow *window) +{ + if (m_valid && !m_inited) { + m_valid = loadData(); + } + + redirect(window); + setShader(window, m_shader.get()); +} + +void SystemBellEffect::unflash(EffectWindow *window) +{ + unredirect(window); +} + +bool SystemBellEffect::loadData() +{ + ensureResources(); + m_inited = true; + + if (m_visibleBell) { + if (m_mode == Invert) { + m_shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/systembell/shaders/invert.frag")); + } else { + m_shader = ShaderManager::instance()->generateShaderFromFile(ShaderTrait::MapTexture, QString(), QStringLiteral(":/effects/systembell/shaders/color.frag")); + ShaderBinder binder(m_shader.get()); + m_shader->setUniform(GLShader::ColorUniform::Color, m_color); + } + + if (!m_shader->isValid()) { + qCCritical(KWIN_SYSTEMBELL) << "The shader failed to load!"; + return false; + } + } + + return true; +} + +void SystemBellEffect::slotWindowClosed(EffectWindow *w) +{ + m_windows.removeOne(w); +} + +void SystemBellEffect::triggerScreen() +{ + if (m_allWindows) { + return; + } + + if (m_audibleBell) { + playAudibleBell(); + } + + if (m_visibleBell) { + m_allWindows = true; + + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + flash(window); + } + + QTimer::singleShot(m_duration, this, [this] { + const auto windows = effects->stackingOrder(); + for (EffectWindow *window : windows) { + unflash(window); + } + m_allWindows = false; + effects->addRepaintFull(); + }); + + effects->addRepaintFull(); + } +} + +void SystemBellEffect::triggerWindow() +{ + const auto window = effects->activeWindow(); + + if (!window || m_windows.contains(window)) { + return; + } + + if (m_audibleBell) { + playAudibleBell(); + } + + if (m_visibleBell) { + m_windows.append(window); + flash(window); + + QTimer::singleShot(m_duration, this, [this, window] { + // window may be closed by now + if (m_windows.contains(window)) { + unflash(window); + m_windows.removeOne(window); + window->addRepaintFull(); + } + }); + + window->addRepaintFull(); + } +} + +bool SystemBellEffect::isActive() const +{ + return m_valid && (m_allWindows || !m_windows.isEmpty()); +} + +bool SystemBellEffect::provides(Feature f) +{ + return f == SystemBell; +} + +bool SystemBellEffect::perform(Feature feature, const QVariantList &arguments) +{ + triggerScreen(); + return true; +} + +void SystemBellEffect::playAudibleBell() +{ + if (m_customBell) { + ca_context_play(m_caContext, + 0, + CA_PROP_MEDIA_FILENAME, + QFile::encodeName(QUrl(m_customBellFile).toLocalFile()).constData(), + CA_PROP_MEDIA_ROLE, + "event", + nullptr); + } else { + const QString themeName = m_kdeglobals.group(QStringLiteral("Sounds")).readEntry("Theme", QStringLiteral("ocean")); + ca_context_play(m_caContext, + 0, + CA_PROP_EVENT_ID, + "bell", + CA_PROP_MEDIA_ROLE, + "event", + CA_PROP_CANBERRA_XDG_THEME_NAME, + themeName.toUtf8().constData(), + nullptr); + } +} + +} // namespace + +#include "moc_systembell.cpp" diff --git a/src/plugins/systembell/systembell.h b/src/plugins/systembell/systembell.h new file mode 100644 index 0000000000..e4bc8c4829 --- /dev/null +++ b/src/plugins/systembell/systembell.h @@ -0,0 +1,83 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2024 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "effect/offscreeneffect.h" + +#include +#include + +class ca_context; + +namespace KWin +{ + +class GLShader; + +class SystemBellEffect : public OffscreenEffect +{ + Q_OBJECT +public: + SystemBellEffect(); + ~SystemBellEffect() override; + + bool isActive() const override; + int requestedEffectChainPosition() const override; + bool provides(Feature f) override; + bool perform(Feature feature, const QVariantList &arguments) override; + void reconfigure(ReconfigureFlags flags) override; + + static bool supported(); + +public Q_SLOTS: + void triggerScreen(); + void triggerWindow(); + +private Q_SLOTS: + void slotWindowClosed(KWin::EffectWindow *w); + +protected: + bool loadData(); + +private: + enum Mode { + Invert, + Color, + }; + + void flash(EffectWindow *window); + void unflash(EffectWindow *window); + void loadConfig(const KConfigGroup &group); + void playAudibleBell(); + + bool m_inited = false; + bool m_valid = true; + std::unique_ptr m_shader; + bool m_allWindows = false; + QList m_windows; + QColor m_color; + int m_duration; + Mode m_mode; + ca_context *m_caContext = nullptr; + bool m_visibleBell = false; + bool m_audibleBell = false; + bool m_customBell = false; + QString m_customBellFile; + KConfigWatcher::Ptr m_configWatcher; + KConfig m_kdeglobals; + KConfigGroup m_bellConfig; +}; + +inline int SystemBellEffect::requestedEffectChainPosition() const +{ + return 99; +} + +} // namespace diff --git a/src/plugins/systembell/systembell.qrc b/src/plugins/systembell/systembell.qrc new file mode 100644 index 0000000000..a2ac0706c3 --- /dev/null +++ b/src/plugins/systembell/systembell.qrc @@ -0,0 +1,8 @@ + + + shaders/color.frag + shaders/color_core.frag + shaders/invert.frag + shaders/invert_core.frag + +