From 53d19fbb9a4d85504bfc001aa108d13567e44d30 Mon Sep 17 00:00:00 2001 From: Nicolas Fella Date: Tue, 27 Dec 2022 03:30:56 +0100 Subject: [PATCH] Implement sticky keys on Wayland Sticky keys allow to trigger key combinations one key at a time. This is an accessibility feature used by people that cannot press multiple keys simultaneously. On X11 this is handled by the X server, configured via kaccess. On Wayland we get to handle this ourselves. wl_keyboard events already carry the modifier's latched/locked state, so all we need to do is to make sure the right state is set Xkb gains a new method to set the state. The business logic is implemented in a new plugin that filters for keys and sets the Xkb state accordingly. BUG: 444335 --- src/plugins/CMakeLists.txt | 1 + src/plugins/stickykeys/CMakeLists.txt | 18 +++++++ src/plugins/stickykeys/main.cpp | 34 +++++++++++++ src/plugins/stickykeys/metadata.json | 6 +++ src/plugins/stickykeys/stickykeys.cpp | 73 +++++++++++++++++++++++++++ src/plugins/stickykeys/stickykeys.h | 33 ++++++++++++ src/xkb.cpp | 48 ++++++++++++++++++ src/xkb.h | 2 + 8 files changed, 215 insertions(+) create mode 100644 src/plugins/stickykeys/CMakeLists.txt create mode 100644 src/plugins/stickykeys/main.cpp create mode 100644 src/plugins/stickykeys/metadata.json create mode 100644 src/plugins/stickykeys/stickykeys.cpp create mode 100644 src/plugins/stickykeys/stickykeys.h diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index bcb6f52a9c..9b64a5b479 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -100,6 +100,7 @@ add_subdirectory(slidingpopups) add_subdirectory(snaphelper) add_subdirectory(squash) add_subdirectory(startupfeedback) +add_subdirectory(stickykeys) add_subdirectory(synchronizeskipswitcher) add_subdirectory(thumbnailaside) add_subdirectory(tileseditor) diff --git a/src/plugins/stickykeys/CMakeLists.txt b/src/plugins/stickykeys/CMakeLists.txt new file mode 100644 index 0000000000..15671994d4 --- /dev/null +++ b/src/plugins/stickykeys/CMakeLists.txt @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2022 Nicolas Fella +# SPDX-License-Identifier: BSD-3-Clause + +kcoreaddons_add_plugin(StickyKeysPlugin INSTALL_NAMESPACE "kwin/plugins") + +ecm_qt_declare_logging_category(StickyKeysPlugin + HEADER stickykeys_debug.h + IDENTIFIER KWIN_STICKYKEYS + CATEGORY_NAME kwin_stickykeys + DEFAULT_SEVERITY Warning +) + +target_sources(StickyKeysPlugin PRIVATE + main.cpp + stickykeys.cpp +) +target_link_libraries(StickyKeysPlugin PRIVATE kwin KF6::WindowSystem) + diff --git a/src/plugins/stickykeys/main.cpp b/src/plugins/stickykeys/main.cpp new file mode 100644 index 0000000000..1058892380 --- /dev/null +++ b/src/plugins/stickykeys/main.cpp @@ -0,0 +1,34 @@ +/* + SPDX-FileCopyrightText: 2022 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include +#include + +#include "stickykeys.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 create() const override + { + switch (KWin::kwinApp()->operationMode()) { + case KWin::Application::OperationModeXwayland: + [[fallthrough]]; + case KWin::Application::OperationModeWaylandOnly: + return std::make_unique(); + case KWin::Application::OperationModeX11: + [[fallthrough]]; + default: + return nullptr; + } + } +}; + +#include "main.moc" diff --git a/src/plugins/stickykeys/metadata.json b/src/plugins/stickykeys/metadata.json new file mode 100644 index 0000000000..942fea311b --- /dev/null +++ b/src/plugins/stickykeys/metadata.json @@ -0,0 +1,6 @@ +{ + "KPlugin": { + "EnabledByDefault": true, + "Id": "kwin5_plugin_stickykeys" + } +} diff --git a/src/plugins/stickykeys/stickykeys.cpp b/src/plugins/stickykeys/stickykeys.cpp new file mode 100644 index 0000000000..a86234df21 --- /dev/null +++ b/src/plugins/stickykeys/stickykeys.cpp @@ -0,0 +1,73 @@ +/* + SPDX-FileCopyrightText: 2022 Nicolas Fella + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "stickykeys.h" +#include "keyboard_input.h" +#include "xkb.h" + +StickyKeysFilter::StickyKeysFilter() + : 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.parent().name() == groupName) { + loadConfig(group); + } + }); + loadConfig(m_configWatcher->config()->group(groupName)); + + for (int mod : std::as_const(m_modifiers)) { + m_keyStates[mod] = None; + } +} + +void StickyKeysFilter::loadConfig(const KConfigGroup &group) +{ + KWin::input()->uninstallInputEventFilter(this); + + if (group.readEntry("StickyKeys", false)) { + KWin::input()->prependInputEventFilter(this); + } +} + +Qt::KeyboardModifier keyToModifier(Qt::Key key) +{ + if (key == Qt::Key_Shift) { + return Qt::ShiftModifier; + } else if (key == Qt::Key_Alt) { + return Qt::AltModifier; + } else if (key == Qt::Key_Control) { + return Qt::ControlModifier; + } else if (key == Qt::Key_AltGr) { + return Qt::GroupSwitchModifier; + } else if (key == Qt::Key_Meta) { + return Qt::MetaModifier; + } + + return Qt::NoModifier; +} + +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; + + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier((Qt::Key)event->key()), true); + } + } else if (event->type() == QKeyEvent::KeyPress) { + // a non-modifier key was pressed, unlatch all modifiers + for (auto it = m_keyStates.keyValueBegin(); it != m_keyStates.keyValueEnd(); ++it) { + it->second = KeyState::None; + + KWin::input()->keyboard()->xkb()->setModifierLatched(keyToModifier((Qt::Key)it->first), false); + } + } + + return false; +} diff --git a/src/plugins/stickykeys/stickykeys.h b/src/plugins/stickykeys/stickykeys.h new file mode 100644 index 0000000000..b1047bd342 --- /dev/null +++ b/src/plugins/stickykeys/stickykeys.h @@ -0,0 +1,33 @@ +/* + SPDX-FileCopyrightText: 2022 Nicolas Fella + + 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 StickyKeysFilter : public KWin::Plugin, public KWin::InputEventFilter +{ + Q_OBJECT +public: + explicit StickyKeysFilter(); + + bool keyEvent(KWin::KeyEvent *event) override; + + enum KeyState { + None, + Latched, + }; + +private: + void loadConfig(const KConfigGroup &group); + + KConfigWatcher::Ptr m_configWatcher; + QMap m_keyStates; + QVector m_modifiers = {Qt::Key_Shift, Qt::Key_Control, Qt::Key_Alt, Qt::Key_AltGr, Qt::Key_Meta}; +}; diff --git a/src/xkb.cpp b/src/xkb.cpp index f2dc60c822..9593437634 100644 --- a/src/xkb.cpp +++ b/src/xkb.cpp @@ -629,6 +629,54 @@ bool Xkb::switchToLayout(xkb_layout_index_t layout) return true; } +void Xkb::setModifierLatched(Qt::KeyboardModifier mod, bool latched) +{ + 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.latched}; + if (mask.size() > modifier) { + mask[modifier] = latched; + m_modifierState.latched = mask.to_ulong(); + xkb_state_update_mask(m_state, m_modifierState.depressed, m_modifierState.latched, m_modifierState.locked, 0, 0, m_currentLayout); + m_modifierState.latched = xkb_state_serialize_mods(m_state, xkb_state_component(XKB_STATE_MODS_LATCHED)); + } + } +} + quint32 Xkb::numberOfLayouts() const { if (!m_keymap) { diff --git a/src/xkb.h b/src/xkb.h index 3bfe0c57c8..0f2c91f0c5 100644 --- a/src/xkb.h +++ b/src/xkb.h @@ -67,6 +67,8 @@ public: void switchToPreviousLayout(); bool switchToLayout(xkb_layout_index_t layout); + void setModifierLatched(Qt::KeyboardModifier mod, bool latched); + LEDs leds() const { return m_leds;