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" + } +}