Implement bounce keys on Wayland

Bouce keys suppresses additional key presses during a given interval

This is used by people with motor impairments or bad keyboards

It works by remembering the last input timestamp for a key

If an event's timestamp is too close to the last timestamp for that key the event is rejected

BUG: 474752
This commit is contained in:
Nicolas Fella 2023-11-23 13:07:35 +00:00
parent b33c5d9fbb
commit e7942c3485
9 changed files with 268 additions and 1 deletions

View file

@ -69,6 +69,7 @@ integrationTest(NAME testDontCrashGlxgears SRCS dont_crash_glxgears.cpp LIBS KF6
if (KWIN_BUILD_SCREENLOCKER)
integrationTest(NAME testLockScreen SRCS lockscreen.cpp LIBS KF6::GlobalAccel)
endif()
integrationTest(NAME testBounceKeys SRCS bounce_keys_test.cpp)
integrationTest(NAME testDecorationInput SRCS decoration_input_test.cpp LIBS KDecoration2::KDecoration KDecoration2::KDecoration2Private)
integrationTest(NAME testInternalWindow SRCS internal_window.cpp)
integrationTest(NAME testTouchInput SRCS touch_input_test.cpp)

View file

@ -0,0 +1,127 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2016 Martin Gräßlin <mgraesslin@kde.org>
SPDX-FileCopyrightText: 2023 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kwin_wayland_test.h"
#include "keyboard_input.h"
#include "pluginmanager.h"
#include "pointer_input.h"
#include "wayland_server.h"
#include "window.h"
#include "workspace.h"
#include <KWayland/Client/keyboard.h>
#include <KWayland/Client/seat.h>
#include <linux/input.h>
namespace KWin
{
static const QString s_socketName = QStringLiteral("wayland_test_kwin_bounce_keys-0");
class BounceKeysTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void init();
void cleanup();
void testBounce();
};
void BounceKeysTest::initTestCase()
{
KConfig kaccessConfig("kaccessrc");
kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("BounceKeys", true);
kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("BounceKeysDelay", 200);
kaccessConfig.sync();
qRegisterMetaType<KWin::Window *>();
QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
QVERIFY(waylandServer()->init(s_socketName));
Test::setOutputConfig({
QRect(0, 0, 1280, 1024),
QRect(1280, 0, 1280, 1024),
});
qputenv("XKB_DEFAULT_RULES", "evdev");
kwinApp()->start();
QVERIFY(applicationStartedSpy.wait());
}
void BounceKeysTest::init()
{
QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat));
QVERIFY(Test::waitForWaylandKeyboard());
}
void BounceKeysTest::cleanup()
{
Test::destroyWaylandConnection();
}
void BounceKeysTest::testBounce()
{
std::unique_ptr<KWayland::Client::Keyboard> keyboard(Test::waylandSeat()->createKeyboard());
std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
QVERIFY(surface != nullptr);
std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
QVERIFY(shellSurface != nullptr);
Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue);
QVERIFY(waylandWindow);
QVERIFY(keyboard);
QSignalSpy enteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered);
QVERIFY(enteredSpy.wait());
QSignalSpy keySpy(keyboard.get(), &KWayland::Client::Keyboard::keyChanged);
QVERIFY(keySpy.isValid());
quint32 timestamp = 0;
// Press a key, verify that it goes through
Test::keyboardKeyPressed(KEY_A, timestamp);
QVERIFY(keySpy.wait());
QCOMPARE(keySpy.first()[0], KEY_A);
QCOMPARE(keySpy.first()[1].value<KWayland::Client::Keyboard::KeyState>(), KWayland::Client::Keyboard::KeyState::Pressed);
keySpy.clear();
Test::keyboardKeyReleased(KEY_A, timestamp++);
QVERIFY(keySpy.wait());
QCOMPARE(keySpy.first()[0], KEY_A);
QCOMPARE(keySpy.first()[1].value<KWayland::Client::Keyboard::KeyState>(), KWayland::Client::Keyboard::KeyState::Released);
keySpy.clear();
// Press it again within the bounce interval, verify that it does *not* go through
timestamp += 100;
Test::keyboardKeyPressed(KEY_A, timestamp);
QVERIFY(!keySpy.wait(100));
keySpy.clear();
// Press it again after the bouce interval, verify that it does go through
timestamp += 1000;
Test::keyboardKeyPressed(KEY_A, timestamp);
QVERIFY(keySpy.wait());
QCOMPARE(keySpy.first()[0], KEY_A);
QCOMPARE(keySpy.first()[1].value<KWayland::Client::Keyboard::KeyState>(), KWayland::Client::Keyboard::KeyState::Pressed);
keySpy.clear();
Test::keyboardKeyReleased(KEY_A, timestamp++);
QVERIFY(keySpy.wait());
QCOMPARE(keySpy.first()[0], KEY_A);
QCOMPARE(keySpy.first()[1].value<KWayland::Client::Keyboard::KeyState>(), KWayland::Client::Keyboard::KeyState::Released);
keySpy.clear();
}
}
WAYLANDTEST_MAIN(KWin::BounceKeysTest)
#include "bounce_keys_test.moc"

View file

@ -129,7 +129,7 @@ private:
const std::chrono::microseconds m_timestamp;
};
class KeyEvent : public QKeyEvent
class KWIN_EXPORT KeyEvent : public QKeyEvent
{
public:
explicit KeyEvent(QEvent::Type type, Qt::Key key, Qt::KeyboardModifiers modifiers, quint32 code, quint32 keysym,

View file

@ -50,6 +50,7 @@ add_subdirectory(private)
add_subdirectory(backgroundcontrast)
add_subdirectory(blendchanges)
add_subdirectory(blur)
add_subdirectory(bouncekeys)
add_subdirectory(buttonrebinds)
add_subdirectory(colorblindnesscorrection)
add_subdirectory(colorpicker)

View file

@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: 2023 Nicolas Fella <nicolas.fella@gmx.de>
# SPDX-License-Identifier: BSD-3-Clause
kcoreaddons_add_plugin(BounceKeysPlugin INSTALL_NAMESPACE "kwin/plugins")
ecm_qt_declare_logging_category(BounceKeysPlugin
HEADER bouncekeys_debug.h
IDENTIFIER KWIN_BOUNCEKEYS
CATEGORY_NAME kwin_bouncekeys
DEFAULT_SEVERITY Warning
)
target_sources(BounceKeysPlugin PRIVATE
main.cpp
bouncekeys.cpp
)
target_link_libraries(BounceKeysPlugin PRIVATE kwin KF6::WindowSystem)

View file

@ -0,0 +1,55 @@
/*
SPDX-FileCopyrightText: 2023 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "bouncekeys.h"
#include "keyboard_input.h"
BounceKeysFilter::BounceKeysFilter()
: m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kaccessrc")))
{
const QLatin1String groupName("Keyboard");
connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) {
if (group.name() == groupName) {
loadConfig(group);
}
});
loadConfig(m_configWatcher->config()->group(groupName));
}
void BounceKeysFilter::loadConfig(const KConfigGroup &group)
{
KWin::input()->uninstallInputEventFilter(this);
if (group.readEntry<bool>("BounceKeys", false)) {
KWin::input()->prependInputEventFilter(this);
m_delay = std::chrono::milliseconds(group.readEntry<int>("BounceKeysDelay", 500));
} else {
m_lastEvent.clear();
}
}
bool BounceKeysFilter::keyEvent(KWin::KeyEvent *event)
{
if (event->type() != KWin::KeyEvent::KeyPress) {
return false;
}
auto it = m_lastEvent.find(event->key());
if (it == m_lastEvent.end()) {
// first time is always good
m_lastEvent[event->key()] = event->timestamp();
return false;
}
auto last = *it;
*it = event->timestamp();
return event->timestamp() - last < m_delay;
}
#include "moc_bouncekeys.cpp"

View file

@ -0,0 +1,28 @@
/*
SPDX-FileCopyrightText: 2023 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#pragma once
#include "plugin.h"
#include "input.h"
#include "input_event.h"
class BounceKeysFilter : public KWin::Plugin, public KWin::InputEventFilter
{
Q_OBJECT
public:
explicit BounceKeysFilter();
bool keyEvent(KWin::KeyEvent *event) override;
private:
void loadConfig(const KConfigGroup &group);
KConfigWatcher::Ptr m_configWatcher;
std::chrono::milliseconds m_delay;
QHash<int, std::chrono::microseconds> m_lastEvent;
};

View file

@ -0,0 +1,32 @@
/*
SPDX-FileCopyrightText: 2023 Nicolas Fella <nicolas.fella@gmx.de>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include "main.h"
#include "plugin.h"
#include "bouncekeys.h"
class KWIN_EXPORT StickyKeysFactory : public KWin::PluginFactory
{
Q_OBJECT
Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json")
Q_INTERFACES(KWin::PluginFactory)
public:
std::unique_ptr<KWin::Plugin> create() const override
{
switch (KWin::kwinApp()->operationMode()) {
case KWin::Application::OperationModeXwayland:
case KWin::Application::OperationModeWaylandOnly:
return std::make_unique<BounceKeysFilter>();
case KWin::Application::OperationModeX11:
default:
return nullptr;
}
}
};
#include "main.moc"

View file

@ -0,0 +1,5 @@
{
"KPlugin": {
"EnabledByDefault": true
}
}