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
This commit is contained in:
Nicolas Fella 2024-07-08 13:55:10 +02:00
parent 14a10b3b4b
commit 80b5910594
13 changed files with 445 additions and 1 deletions

View file

@ -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")

View file

@ -546,7 +546,8 @@ public:
ScreenInversion,
Blur,
Contrast,
HighlightWindows
HighlightWindows,
SystemBell,
};
/**

View file

@ -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)

View file

@ -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
)

View file

@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: 2024 Nicolas Fella <nicolas.fella@kdab.com>
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"

View file

@ -0,0 +1,12 @@
{
"KPlugin": {
"Category": "Accessibility",
"Description": "Provides the bell",
"EnabledByDefault": true,
"License": "GPL",
"Name": "System Bell"
},
"org.kde.kwin.effect": {
"internal": true
}
}

View file

@ -0,0 +1,6 @@
uniform vec4 geometryColor;
void main()
{
gl_FragColor = geometryColor;
}

View file

@ -0,0 +1,6 @@
uniform vec4 geometryColor;
void main()
{
gl_FragColor = geometryColor;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -0,0 +1,241 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2024 Nicolas Fella <nicolas.fella@kdab.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "systembell.h"
#include "effect/effecthandler.h"
#include "opengl/glshader.h"
#include "opengl/glshadermanager.h"
#include <QDBusConnection>
#include <QFile>
#include <QGuiApplication>
#include <QTimer>
#include <canberra.h>
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<QColor>("VisibleBellColor", QColor(Qt::red));
m_mode = m_bellConfig.readEntry<bool>("VisibleBellInvert", false) ? Invert : Color;
m_duration = m_bellConfig.readEntry<int>("VisibleBellPause", 500);
m_audibleBell = m_bellConfig.readEntry<bool>("SystemBell", true);
m_customBell = m_bellConfig.readEntry<bool>("ArtsBell", false);
m_customBellFile = m_bellConfig.readEntry<QString>("ArtsBellFile", QString());
m_visibleBell = m_bellConfig.readEntry<bool>("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"

View file

@ -0,0 +1,83 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2024 Nicolas Fella <nicolas.fella@kdab.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "effect/offscreeneffect.h"
#include <KConfigGroup>
#include <KConfigWatcher>
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<GLShader> m_shader;
bool m_allWindows = false;
QList<EffectWindow *> 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

View file

@ -0,0 +1,8 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/effects/systembell/">
<file>shaders/color.frag</file>
<file>shaders/color_core.frag</file>
<file>shaders/invert.frag</file>
<file>shaders/invert_core.frag</file>
</qresource>
</RCC>