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:
parent
14a10b3b4b
commit
80b5910594
13 changed files with 445 additions and 1 deletions
|
@ -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")
|
||||
|
|
|
@ -546,7 +546,8 @@ public:
|
|||
ScreenInversion,
|
||||
Blur,
|
||||
Contrast,
|
||||
HighlightWindows
|
||||
HighlightWindows,
|
||||
SystemBell,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)
|
||||
|
|
14
src/plugins/systembell/CMakeLists.txt
Normal file
14
src/plugins/systembell/CMakeLists.txt
Normal 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
|
||||
)
|
18
src/plugins/systembell/main.cpp
Normal file
18
src/plugins/systembell/main.cpp
Normal 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"
|
12
src/plugins/systembell/metadata.json
Normal file
12
src/plugins/systembell/metadata.json
Normal 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
|
||||
}
|
||||
}
|
6
src/plugins/systembell/shaders/color.frag
Normal file
6
src/plugins/systembell/shaders/color.frag
Normal file
|
@ -0,0 +1,6 @@
|
|||
uniform vec4 geometryColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_FragColor = geometryColor;
|
||||
}
|
6
src/plugins/systembell/shaders/color_core.frag
Normal file
6
src/plugins/systembell/shaders/color_core.frag
Normal file
|
@ -0,0 +1,6 @@
|
|||
uniform vec4 geometryColor;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_FragColor = geometryColor;
|
||||
}
|
24
src/plugins/systembell/shaders/invert.frag
Normal file
24
src/plugins/systembell/shaders/invert.frag
Normal 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);
|
||||
}
|
28
src/plugins/systembell/shaders/invert_core.frag
Normal file
28
src/plugins/systembell/shaders/invert_core.frag
Normal 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);
|
||||
}
|
241
src/plugins/systembell/systembell.cpp
Normal file
241
src/plugins/systembell/systembell.cpp
Normal 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"
|
83
src/plugins/systembell/systembell.h
Normal file
83
src/plugins/systembell/systembell.h
Normal 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
|
8
src/plugins/systembell/systembell.qrc
Normal file
8
src/plugins/systembell/systembell.qrc
Normal 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>
|
Loading…
Reference in a new issue