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.
This commit is contained in:
David Redondo 2022-04-28 12:24:18 +02:00
parent 80d28499e1
commit bc792a2bc8
6 changed files with 313 additions and 0 deletions

View file

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

View file

@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: 2022 David Redondo <kde@david-redono.de>
# 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)

View file

@ -0,0 +1,200 @@
/*
SPDX-FileCopyrightText: 2022 David Redondo <kde@david-redono.de>
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 <KKeyServer>
#include <QMetaEnum>
#include <linux/input-event-codes.h>
#include <optional>
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<int> 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<Qt::MouseButtons>();
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<Qt::MouseButton>(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;
}

View file

@ -0,0 +1,49 @@
/*
SPDX-FileCopyrightText: 2022 David Redondo <kde@david-redono.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 "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<Qt::MouseButton, QKeySequence> m_mouseMapping;
KConfigWatcher::Ptr m_configWatcher;
};

View file

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

View file

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