From bc792a2bc85b0b24a9d2ce7d48a61663fc4b4f8b Mon Sep 17 00:00:00 2001 From: David Redondo Date: Thu, 28 Apr 2022 12:24:18 +0200 Subject: [PATCH] Allow rebinding of extra mouse buttons Some mice have more than the three standard buttons. While some applications can use a subset of those (mostly the backwards and forwards buttons) in many cases pressing them will do nothing. This makes it possible to assign key combinations to buttons that will cause synthetic key event when pressed. --- src/plugins/CMakeLists.txt | 1 + src/plugins/buttonrebinds/CMakeLists.txt | 18 ++ .../buttonrebinds/buttonrebindsfilter.cpp | 200 ++++++++++++++++++ .../buttonrebinds/buttonrebindsfilter.h | 49 +++++ src/plugins/buttonrebinds/main.cpp | 39 ++++ src/plugins/buttonrebinds/metadata.json | 6 + 6 files changed, 313 insertions(+) create mode 100644 src/plugins/buttonrebinds/CMakeLists.txt create mode 100644 src/plugins/buttonrebinds/buttonrebindsfilter.cpp create mode 100644 src/plugins/buttonrebinds/buttonrebindsfilter.h create mode 100644 src/plugins/buttonrebinds/main.cpp create mode 100644 src/plugins/buttonrebinds/metadata.json diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt index 675ebc16a1..bb89cefd07 100644 --- a/src/plugins/CMakeLists.txt +++ b/src/plugins/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory(windowsystem) add_subdirectory(kpackage) add_subdirectory(nightcolor) add_subdirectory(colord-integration) +add_subdirectory(buttonrebinds) if (KWIN_BUILD_DECORATIONS) add_subdirectory(kdecorations) endif() diff --git a/src/plugins/buttonrebinds/CMakeLists.txt b/src/plugins/buttonrebinds/CMakeLists.txt new file mode 100644 index 0000000000..c60db5dfa6 --- /dev/null +++ b/src/plugins/buttonrebinds/CMakeLists.txt @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2022 David Redondo +# SPDX-License-Identifier: BSD-3-Clause + +kcoreaddons_add_plugin(MouseButtonToKeyPlugin INSTALL_NAMESPACE "kwin/plugins") + +ecm_qt_declare_logging_category(MouseButtonToKeyPlugin + HEADER buttonrebinds_debug.h + IDENTIFIER KWIN_BUTTONREBINDS + CATEGORY_NAME kwin_buttonrebinds + DEFAULT_SEVERITY Warning +) + +target_sources(MouseButtonToKeyPlugin PRIVATE + main.cpp + buttonrebindsfilter.cpp +) +target_link_libraries(MouseButtonToKeyPlugin PRIVATE kwin KF5::WindowSystem) + diff --git a/src/plugins/buttonrebinds/buttonrebindsfilter.cpp b/src/plugins/buttonrebinds/buttonrebindsfilter.cpp new file mode 100644 index 0000000000..0408e52ee0 --- /dev/null +++ b/src/plugins/buttonrebinds/buttonrebindsfilter.cpp @@ -0,0 +1,200 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "buttonrebindsfilter.h" +#include "buttonrebinds_debug.h" + +#include "input_event.h" +#include "keyboard_input.h" + +#include + +#include + +#include + +#include + +QString InputDevice::name() const +{ + return QStringLiteral("Button rebinding device"); +} + +QString InputDevice::sysName() const +{ + return QString(); +} + +KWin::LEDs InputDevice::leds() const +{ + return {}; +} + +void InputDevice::setLeds(KWin::LEDs leds) +{ + Q_UNUSED(leds) +} + +void InputDevice::setEnabled(bool enabled) +{ + Q_UNUSED(enabled) +} + +bool InputDevice::isEnabled() const +{ + return true; +} + +bool InputDevice::isAlphaNumericKeyboard() const +{ + return true; +} + +bool InputDevice::isKeyboard() const +{ + return true; +} + +bool InputDevice::isLidSwitch() const +{ + return false; +} + +bool InputDevice::isPointer() const +{ + return false; +} + +bool InputDevice::isTabletModeSwitch() const +{ + return false; +} + +bool InputDevice::isTabletPad() const +{ + return false; +} + +bool InputDevice::isTabletTool() const +{ + return false; +} + +bool InputDevice::isTouch() const +{ + return false; +} + +bool InputDevice::isTouchpad() const +{ + return false; +} + +static std::optional keycodeFromKeysym(xkb_keysym_t keysym) +{ + auto xkb = KWin::input()->keyboard()->xkb(); + auto layout = xkb_state_serialize_layout(xkb->state(), XKB_STATE_LAYOUT_EFFECTIVE); + const xkb_keycode_t max = xkb_keymap_max_keycode(xkb->keymap()); + for (xkb_keycode_t keycode = xkb_keymap_min_keycode(xkb->keymap()); keycode < max; keycode++) { + uint levelCount = xkb_keymap_num_levels_for_key(xkb->keymap(), keycode, layout); + for (uint currentLevel = 0; currentLevel < levelCount; currentLevel++) { + const xkb_keysym_t *syms; + uint num_syms = xkb_keymap_key_get_syms_by_level(xkb->keymap(), keycode, layout, currentLevel, &syms); + for (uint sym = 0; sym < num_syms; sym++) { + if (syms[sym] == keysym) { + return {keycode - 8}; + } + } + } + } + return {}; +} + +ButtonRebindsFilter::ButtonRebindsFilter() + : KWin::Plugin() + , KWin::InputEventFilter() + , m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kcminputrc"))) +{ + KWin::input()->addInputDevice(&m_inputDevice); + const QLatin1String groupName("ButtonRebinds"); + connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) { + if (group.parent().name() != groupName) { + return; + } + loadConfig(group.parent()); + }); + loadConfig(m_configWatcher->config()->group(groupName)); +} + +void ButtonRebindsFilter::loadConfig(const KConfigGroup &group) +{ + KWin::input()->uninstallInputEventFilter(this); + m_mouseMapping.clear(); + auto mouseButtonEnum = QMetaEnum::fromType(); + auto mouseGroup = group.group("Mouse"); + for (int i = 1; i <= 24; ++i) { + const QByteArray buttonName = QByteArray("ExtraButton") + QByteArray::number(i); + auto entry = mouseGroup.readEntry(buttonName.constData(), QStringList()); + if (entry.size() == 2 && entry.first() == QLatin1String("Key")) { + auto keys = QKeySequence::fromString(entry.at(1), QKeySequence::PortableText); + if (!keys.isEmpty()) { + m_mouseMapping.insert(static_cast(mouseButtonEnum.keyToValue(buttonName)), keys); + } + } + } + if (m_mouseMapping.size() != 0) { + KWin::input()->prependInputEventFilter(this); + } +} + +bool ButtonRebindsFilter::pointerEvent(QMouseEvent *event, quint32 nativeButton) +{ + Q_UNUSED(nativeButton); + + if (event->type() != QEvent::MouseButtonPress && event->type() != QEvent::MouseButtonRelease) { + return false; + } + + const QKeySequence keys = m_mouseMapping.value(event->button()); + if (keys.isEmpty()) { + return false; + } + const auto &key = keys[0]; + + int sym; + if (!KKeyServer::keyQtToSymX(keys[0], &sym)) { + qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << keys << "to keysym"; + return false; + } + // KKeyServer returns upper case syms, lower it to not confuse modifiers handling + auto keyCode = keycodeFromKeysym(sym); + if (!keyCode) { + qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << keys << "sym: " << sym << "to keycode"; + return false; + } + + auto sendKey = [this, event](xkb_keycode_t key) { + auto state = event->type() == QEvent::MouseButtonPress ? KWin::InputRedirection::KeyboardKeyPressed : KWin::InputRedirection::KeyboardKeyReleased; + Q_EMIT m_inputDevice.keyChanged(key, state, event->timestamp(), &m_inputDevice); + }; + + if (key & Qt::ShiftModifier) { + sendKey(KEY_LEFTSHIFT); + } + if (key & Qt::ControlModifier) { + sendKey(KEY_LEFTCTRL); + } + if (key & Qt::AltModifier) { + sendKey(KEY_LEFTALT); + } + if (key & Qt::MetaModifier) { + sendKey(XKB_KEY_Super_L); + } + + sendKey(keyCode.value()); + + return true; +} diff --git a/src/plugins/buttonrebinds/buttonrebindsfilter.h b/src/plugins/buttonrebinds/buttonrebindsfilter.h new file mode 100644 index 0000000000..1ad3b05d84 --- /dev/null +++ b/src/plugins/buttonrebinds/buttonrebindsfilter.h @@ -0,0 +1,49 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + 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 "inputdevice.h" + +class InputDevice : public KWin::InputDevice +{ + QString sysName() const override; + QString name() const override; + + bool isEnabled() const override; + void setEnabled(bool enabled) override; + + void setLeds(KWin::LEDs leds) override; + KWin::LEDs leds() const override; + + bool isKeyboard() const override; + bool isAlphaNumericKeyboard() const override; + bool isPointer() const override; + bool isTouchpad() const override; + bool isTouch() const override; + bool isTabletTool() const override; + bool isTabletPad() const override; + bool isTabletModeSwitch() const override; + bool isLidSwitch() const override; +}; + +class ButtonRebindsFilter : public KWin::Plugin, public KWin::InputEventFilter +{ + Q_OBJECT +public: + explicit ButtonRebindsFilter(); + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override; + +private: + void loadConfig(const KConfigGroup &group); + + InputDevice m_inputDevice; + QMap m_mouseMapping; + KConfigWatcher::Ptr m_configWatcher; +}; diff --git a/src/plugins/buttonrebinds/main.cpp b/src/plugins/buttonrebinds/main.cpp new file mode 100644 index 0000000000..9d8d4ddb23 --- /dev/null +++ b/src/plugins/buttonrebinds/main.cpp @@ -0,0 +1,39 @@ +/* + SPDX-FileCopyrightText: 2022 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include +#include + +#include "buttonrebindsfilter.h" + +class KWIN_EXPORT ButtonRebindsFactory : public KWin::PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + explicit ButtonRebindsFactory() + : PluginFactory() + { + } + + 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/buttonrebinds/metadata.json b/src/plugins/buttonrebinds/metadata.json new file mode 100644 index 0000000000..0d5a7a0efc --- /dev/null +++ b/src/plugins/buttonrebinds/metadata.json @@ -0,0 +1,6 @@ +{ + "KPlugin": { + "EnabledByDefault": true, + "Id": "kwin5_plugin_buttonrebinds" + } +}