From d4127d07fd55c342d28fa7a6b66b460473c5d00c Mon Sep 17 00:00:00 2001 From: Nicolas Fella Date: Thu, 23 Nov 2023 08:17:00 +0000 Subject: [PATCH] Implement locking sticky keys on Wayland When pressing a latched modifier a second time lock it When pressing a locked modifier release it BUG: 464452 --- autotests/integration/CMakeLists.txt | 1 + autotests/integration/sticky_keys_test.cpp | 180 +++++++++++++++++++++ src/plugins/stickykeys/stickykeys.cpp | 44 ++++- src/plugins/stickykeys/stickykeys.h | 2 + src/xkb.cpp | 48 ++++++ src/xkb.h | 1 + 6 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 autotests/integration/sticky_keys_test.cpp diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt index 5c7b579810..de5db5ad13 100644 --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -128,6 +128,7 @@ integrationTest(NAME testXwaylandServerCrash SRCS xwaylandserver_crash_test.cpp integrationTest(NAME testXwaylandServerRestart SRCS xwaylandserver_restart_test.cpp LIBS XCB::ICCCM) integrationTest(NAME testFakeInput SRCS fakeinput_test.cpp) integrationTest(NAME testSecurityContext SRCS security_context_test.cpp) +integrationTest(NAME testStickyKeys SRCS sticky_keys_test.cpp) qt_add_dbus_interfaces(DBUS_SRCS ${CMAKE_BINARY_DIR}/src/org.kde.kwin.VirtualKeyboard.xml) integrationTest(NAME testVirtualKeyboardDBus SRCS test_virtualkeyboard_dbus.cpp ${DBUS_SRCS}) diff --git a/autotests/integration/sticky_keys_test.cpp b/autotests/integration/sticky_keys_test.cpp new file mode 100644 index 0000000000..5b8794eb23 --- /dev/null +++ b/autotests/integration/sticky_keys_test.cpp @@ -0,0 +1,180 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2016 Martin Gräßlin + SPDX-FileCopyrightText: 2023 Nicolas Fella + + 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 +#include + +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_sticky_keys-0"); + +class StickyKeysTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testStick(); + void testLock(); +}; + +void StickyKeysTest::initTestCase() +{ + KConfig kaccessConfig("kaccessrc"); + kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("StickyKeys", true); + kaccessConfig.sync(); + + qRegisterMetaType(); + 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 StickyKeysTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandKeyboard()); +} + +void StickyKeysTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void StickyKeysTest::testStick() +{ + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); + QVERIFY(waylandWindow); + + QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); + QVERIFY(modifierSpy.wait()); + modifierSpy.clear(); + + quint32 timestamp = 0; + + // press Ctrl to latch it + Test::keyboardKeyPressed(KEY_LEFTCTRL, ++timestamp); + QVERIFY(modifierSpy.wait()); + // arguments are: quint32 depressed, quint32 latched, quint32 locked, quint32 group + QCOMPARE(modifierSpy.first()[0], 4); // verify that Ctrl is depressed + QCOMPARE(modifierSpy.first()[1], 4); // verify that Ctrl is latched + + modifierSpy.clear(); + // release Ctrl, the modified should still be latched + Test::keyboardKeyReleased(KEY_LEFTCTRL, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that Ctrl is not depressed + QCOMPARE(modifierSpy.first()[1], 4); // verify that Ctrl is still latched + + // press and release a letter, this unlatches the modifier + modifierSpy.clear(); + Test::keyboardKeyPressed(KEY_A, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that Ctrl is not depressed + QCOMPARE(modifierSpy.first()[1], 0); // verify that Ctrl is not latched any more + + Test::keyboardKeyReleased(KEY_A, ++timestamp); +} + +void StickyKeysTest::testLock() +{ + KConfig kaccessConfig("kaccessrc"); + kaccessConfig.group(QStringLiteral("Keyboard")).writeEntry("StickyKeysLatch", true); + kaccessConfig.sync(); + + // reload the plugin to pick up the new config + kwinApp()->pluginManager()->unloadPlugin("StickyKeysPlugin"); + kwinApp()->pluginManager()->loadPlugin("StickyKeysPlugin"); + + QVERIFY(Test::waylandSeat()->hasKeyboard()); + std::unique_ptr keyboard(Test::waylandSeat()->createKeyboard()); + + std::unique_ptr surface(Test::createSurface()); + QVERIFY(surface != nullptr); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + QVERIFY(shellSurface != nullptr); + Window *waylandWindow = Test::renderAndWaitForShown(surface.get(), QSize(10, 10), Qt::blue); + QVERIFY(waylandWindow); + waylandWindow->move(QPoint(0, 0)); + + QSignalSpy modifierSpy(keyboard.get(), &KWayland::Client::Keyboard::modifiersChanged); + QVERIFY(modifierSpy.wait()); + modifierSpy.clear(); + + quint32 timestamp = 0; + + // press Ctrl to latch it + Test::keyboardKeyPressed(KEY_LEFTCTRL, ++timestamp); + QVERIFY(modifierSpy.wait()); + // arguments are: quint32 depressed, quint32 latched, quint32 locked, quint32 group + QCOMPARE(modifierSpy.first()[0], 4); // verify that Ctrl is depressed + QCOMPARE(modifierSpy.first()[1], 4); // verify that Ctrl is latched + + modifierSpy.clear(); + // release Ctrl, the modified should still be latched + Test::keyboardKeyReleased(KEY_LEFTCTRL, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 0); // verify that Ctrl is not depressed + QCOMPARE(modifierSpy.first()[1], 4); // verify that Ctrl is still latched + + // press Ctrl again to lock it + modifierSpy.clear(); + Test::keyboardKeyPressed(KEY_LEFTCTRL, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 4); // verify that Ctrl is depressed + // TODO should it be latched? + QCOMPARE(modifierSpy.first()[2], 4); // verify that Ctrl is locked + + // press and release a letter, this does not unlock the modifier + modifierSpy.clear(); + Test::keyboardKeyPressed(KEY_A, ++timestamp); + QVERIFY(!modifierSpy.wait(10)); + + Test::keyboardKeyReleased(KEY_A, ++timestamp); + QVERIFY(!modifierSpy.wait(10)); + + // press Ctrl again to unlock it + Test::keyboardKeyPressed(KEY_LEFTCTRL, ++timestamp); + QVERIFY(modifierSpy.wait()); + QCOMPARE(modifierSpy.first()[0], 4); // verify that Ctrl is depressed + QCOMPARE(modifierSpy.first()[2], 0); // verify that Ctrl is locked + + Test::keyboardKeyReleased(KEY_LEFTCTRL, ++timestamp); +} +} + +WAYLANDTEST_MAIN(KWin::StickyKeysTest) +#include "sticky_keys_test.moc" diff --git a/src/plugins/stickykeys/stickykeys.cpp b/src/plugins/stickykeys/stickykeys.cpp index a965bd4250..a8e31857c6 100644 --- a/src/plugins/stickykeys/stickykeys.cpp +++ b/src/plugins/stickykeys/stickykeys.cpp @@ -45,6 +45,18 @@ void StickyKeysFilter::loadConfig(const KConfigGroup &group) { KWin::input()->uninstallInputEventFilter(this); + m_lockKeys = group.readEntry("StickyKeysLatch", true); + + if (!m_lockKeys) { + // locking keys is deactivated, unlock all locked keys + for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { + if (it->second == KeyState::Locked) { + it->second = KeyState::None; + KWin::input()->keyboard()->xkb()->setModifierLocked(keyToModifier(static_cast(it->first)), false); + } + } + } + if (group.readEntry("StickyKeys", false)) { KWin::input()->prependInputEventFilter(this); } else { @@ -52,7 +64,7 @@ void StickyKeysFilter::loadConfig(const KConfigGroup &group) for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { if (it->second != KeyState::None) { it->second = KeyState::None; - KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier((Qt::Key)it->first), false); + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(it->first)), false); } } } @@ -61,16 +73,36 @@ void StickyKeysFilter::loadConfig(const KConfigGroup &group) bool StickyKeysFilter::keyEvent(KWin::KeyEvent *event) { if (m_modifiers.contains(event->key())) { - // A modifier was pressed, latch it - if (event->type() == QKeyEvent::KeyPress && m_keyStates[event->key()] != Latched) { - m_keyStates[event->key()] = Latched; + auto keyState = m_keyStates.find(event->key()); - KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier((Qt::Key)event->key()), true); + if (keyState != m_keyStates.end()) { + if (event->type() == QKeyEvent::KeyPress) { + // An unlatched modifier was pressed, latch it + if (keyState.value() == None) { + keyState.value() = Latched; + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(event->key())), true); + } + // A latched modifier was pressed, lock it + else if (keyState.value() == Latched && m_lockKeys) { + keyState.value() = Locked; + KWin::input()->keyboard()->xkb()->setModifierLocked(keyToModifier(static_cast(event->key())), true); + } + // A locked modifier was pressed, unlock it + else if (keyState.value() == Locked && m_lockKeys) { + keyState.value() = None; + KWin::input()->keyboard()->xkb()->setModifierLocked(keyToModifier(static_cast(event->key())), false); + } + } } } else if (event->type() == QKeyEvent::KeyPress) { - // a non-modifier key was pressed, unlatch all modifiers + // a non-modifier key was pressed, unlatch all unlocked modifiers for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { + + if (it->second == Locked) { + continue; + } + it->second = KeyState::None; KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier(static_cast(it->first)), false); diff --git a/src/plugins/stickykeys/stickykeys.h b/src/plugins/stickykeys/stickykeys.h index 50a1b936a2..9a18289b15 100644 --- a/src/plugins/stickykeys/stickykeys.h +++ b/src/plugins/stickykeys/stickykeys.h @@ -22,6 +22,7 @@ public: enum KeyState { None, Latched, + Locked, }; private: @@ -30,4 +31,5 @@ private: KConfigWatcher::Ptr m_configWatcher; QMap m_keyStates; QList m_modifiers = {Qt::Key_Shift, Qt::Key_Control, Qt::Key_Alt, Qt::Key_AltGr, Qt::Key_Meta}; + bool m_lockKeys = false; }; diff --git a/src/xkb.cpp b/src/xkb.cpp index 7ed36999c9..6534622c57 100644 --- a/src/xkb.cpp +++ b/src/xkb.cpp @@ -706,6 +706,54 @@ void Xkb::setModifierLatched(Qt::KeyboardModifier mod, bool latched) } } +void Xkb::setModifierLocked(Qt::KeyboardModifier mod, bool locked) +{ + xkb_mod_index_t modifier = XKB_MOD_INVALID; + + switch (mod) { + case Qt::NoModifier: { + break; + } + case Qt::ShiftModifier: { + modifier = m_shiftModifier; + break; + } + case Qt::AltModifier: { + modifier = m_altModifier; + break; + } + case Qt::ControlModifier: { + modifier = m_controlModifier; + break; + } + case Qt::MetaModifier: { + modifier = m_metaModifier; + break; + } + case Qt::GroupSwitchModifier: { + // TODO + break; + } + case Qt::KeypadModifier: { + modifier = m_numModifier; + break; + } + case Qt::KeyboardModifierMask: { + break; + } + } + + if (modifier != XKB_MOD_INVALID) { + std::bitset mask{m_modifierState.locked}; + if (mask.size() > modifier) { + mask[modifier] = locked; + m_modifierState.locked = mask.to_ulong(); + xkb_state_update_mask(m_state, m_modifierState.depressed, m_modifierState.locked, m_modifierState.locked, 0, 0, m_currentLayout); + m_modifierState.locked = xkb_state_serialize_mods(m_state, xkb_state_component(XKB_STATE_MODS_LOCKED)); + } + } +} + quint32 Xkb::numberOfLayouts() const { if (!m_keymap) { diff --git a/src/xkb.h b/src/xkb.h index 2dbb5073bf..ecbe8e8888 100644 --- a/src/xkb.h +++ b/src/xkb.h @@ -66,6 +66,7 @@ public: bool switchToLayout(xkb_layout_index_t layout); void setModifierLatched(Qt::KeyboardModifier mod, bool latched); + void setModifierLocked(Qt::KeyboardModifier mod, bool locked); LEDs leds() const {