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
This commit is contained in:
Nicolas Fella 2022-12-27 03:30:56 +01:00
parent e6b5cf283e
commit 53d19fbb9a
8 changed files with 215 additions and 0 deletions

View file

@ -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)

View file

@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
# 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)

View file

@ -0,0 +1,34 @@
/*
SPDX-FileCopyrightText: 2022 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 "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<KWin::Plugin> create() const override
{
switch (KWin::kwinApp()->operationMode()) {
case KWin::Application::OperationModeXwayland:
[[fallthrough]];
case KWin::Application::OperationModeWaylandOnly:
return std::make_unique<StickyKeysFilter>();
case KWin::Application::OperationModeX11:
[[fallthrough]];
default:
return nullptr;
}
}
};
#include "main.moc"

View file

@ -0,0 +1,6 @@
{
"KPlugin": {
"EnabledByDefault": true,
"Id": "kwin5_plugin_stickykeys"
}
}

View file

@ -0,0 +1,73 @@
/*
SPDX-FileCopyrightText: 2022 Nicolas Fella <nicolas.fella@gmx.de>
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<bool>("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;
}

View file

@ -0,0 +1,33 @@
/*
SPDX-FileCopyrightText: 2022 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 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<int, KeyState> m_keyStates;
QVector<int> m_modifiers = {Qt::Key_Shift, Qt::Key_Control, Qt::Key_Alt, Qt::Key_AltGr, Qt::Key_Meta};
};

View file

@ -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<sizeof(xkb_mod_mask_t) * 8> 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) {

View file

@ -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;