From 80b59105944a74e5946180889df2f681b1da0a48 Mon Sep 17 00:00:00 2001 From: Nicolas Fella Date: Mon, 8 Jul 2024 13:55:10 +0200 Subject: [PATCH] Add visual bell effect This effect is used to implement the visual bell accessibility feature. It allows to implement it on Wayland and significantly improve it on X11, where it's currently rather broken. It offers two modes: - Inverting the colors (code is based off the invert effect) - Flashing a solid color --- CMakeLists.txt | 2 + src/effect/effect.h | 3 +- src/plugins/CMakeLists.txt | 1 + src/plugins/systembell/CMakeLists.txt | 14 + src/plugins/systembell/main.cpp | 18 ++ src/plugins/systembell/metadata.json | 12 + src/plugins/systembell/shaders/color.frag | 6 + .../systembell/shaders/color_core.frag | 6 + src/plugins/systembell/shaders/invert.frag | 24 ++ .../systembell/shaders/invert_core.frag | 28 ++ src/plugins/systembell/systembell.cpp | 241 ++++++++++++++++++ src/plugins/systembell/systembell.h | 83 ++++++ src/plugins/systembell/systembell.qrc | 8 + 13 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 src/plugins/systembell/CMakeLists.txt create mode 100644 src/plugins/systembell/main.cpp create mode 100644 src/plugins/systembell/metadata.json create mode 100644 src/plugins/systembell/shaders/color.frag create mode 100644 src/plugins/systembell/shaders/color_core.frag create mode 100644 src/plugins/systembell/shaders/invert.frag create mode 100644 src/plugins/systembell/shaders/invert_core.frag create mode 100644 src/plugins/systembell/systembell.cpp create mode 100644 src/plugins/systembell/systembell.h create mode 100644 src/plugins/systembell/systembell.qrc 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 + +