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:
parent
b33c5d9fbb
commit
e7942c3485
9 changed files with 268 additions and 1 deletions
|
@ -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)
|
||||
|
|
127
autotests/integration/bounce_keys_test.cpp
Normal file
127
autotests/integration/bounce_keys_test.cpp
Normal 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"
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
18
src/plugins/bouncekeys/CMakeLists.txt
Normal file
18
src/plugins/bouncekeys/CMakeLists.txt
Normal 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)
|
||||
|
55
src/plugins/bouncekeys/bouncekeys.cpp
Normal file
55
src/plugins/bouncekeys/bouncekeys.cpp
Normal 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"
|
28
src/plugins/bouncekeys/bouncekeys.h
Normal file
28
src/plugins/bouncekeys/bouncekeys.h
Normal 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;
|
||||
};
|
32
src/plugins/bouncekeys/main.cpp
Normal file
32
src/plugins/bouncekeys/main.cpp
Normal 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"
|
5
src/plugins/bouncekeys/metadata.json
Normal file
5
src/plugins/bouncekeys/metadata.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"KPlugin": {
|
||||
"EnabledByDefault": true
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue