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:
parent
e6b5cf283e
commit
53d19fbb9a
8 changed files with 215 additions and 0 deletions
|
@ -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)
|
||||
|
|
18
src/plugins/stickykeys/CMakeLists.txt
Normal file
18
src/plugins/stickykeys/CMakeLists.txt
Normal 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)
|
||||
|
34
src/plugins/stickykeys/main.cpp
Normal file
34
src/plugins/stickykeys/main.cpp
Normal 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"
|
6
src/plugins/stickykeys/metadata.json
Normal file
6
src/plugins/stickykeys/metadata.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"KPlugin": {
|
||||
"EnabledByDefault": true,
|
||||
"Id": "kwin5_plugin_stickykeys"
|
||||
}
|
||||
}
|
73
src/plugins/stickykeys/stickykeys.cpp
Normal file
73
src/plugins/stickykeys/stickykeys.cpp
Normal 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;
|
||||
}
|
33
src/plugins/stickykeys/stickykeys.h
Normal file
33
src/plugins/stickykeys/stickykeys.h
Normal 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};
|
||||
};
|
48
src/xkb.cpp
48
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<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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue