File indexing completed on 2026-01-18 13:12:09

0001 /*
0002     SPDX-FileCopyrightText: 2022 David Redondo <kde@david-redono.de>
0003     SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "buttonrebindsfilter.h"
0009 #include "buttonrebinds_debug.h"
0010 
0011 #include "cursor.h"
0012 #include "input_event.h"
0013 #include "keyboard_input.h"
0014 #include "xkb.h"
0015 
0016 #include <KKeyServer>
0017 
0018 #include <QMetaEnum>
0019 
0020 #include <linux/input-event-codes.h>
0021 
0022 #include <optional>
0023 
0024 // Tells us that we are already in a binding event
0025 class RebindScope
0026 {
0027     static uint s_scopes;
0028 
0029 public:
0030     RebindScope()
0031     {
0032         s_scopes++;
0033     }
0034     ~RebindScope()
0035     {
0036         Q_ASSERT(s_scopes > 0);
0037         s_scopes--;
0038     }
0039     Q_DISABLE_COPY_MOVE(RebindScope)
0040     static bool isRebinding()
0041     {
0042         return s_scopes > 0;
0043     }
0044 };
0045 uint RebindScope::s_scopes = 0;
0046 
0047 quint32 qHash(const Trigger &t)
0048 {
0049     return qHash(t.device) * (t.button + 1);
0050 }
0051 
0052 QString InputDevice::name() const
0053 {
0054     return QStringLiteral("Button rebinding device");
0055 }
0056 
0057 QString InputDevice::sysName() const
0058 {
0059     return {};
0060 }
0061 
0062 KWin::LEDs InputDevice::leds() const
0063 {
0064     return {};
0065 }
0066 
0067 void InputDevice::setLeds(KWin::LEDs leds)
0068 {
0069 }
0070 
0071 void InputDevice::setEnabled(bool enabled)
0072 {
0073 }
0074 
0075 bool InputDevice::isEnabled() const
0076 {
0077     return true;
0078 }
0079 
0080 bool InputDevice::isAlphaNumericKeyboard() const
0081 {
0082     return true;
0083 }
0084 
0085 bool InputDevice::isKeyboard() const
0086 {
0087     return true;
0088 }
0089 
0090 bool InputDevice::isLidSwitch() const
0091 {
0092     return false;
0093 }
0094 
0095 bool InputDevice::isPointer() const
0096 {
0097     return false;
0098 }
0099 
0100 bool InputDevice::isTabletModeSwitch() const
0101 {
0102     return false;
0103 }
0104 
0105 bool InputDevice::isTabletPad() const
0106 {
0107     return false;
0108 }
0109 
0110 bool InputDevice::isTabletTool() const
0111 {
0112     return false;
0113 }
0114 
0115 bool InputDevice::isTouch() const
0116 {
0117     return false;
0118 }
0119 
0120 bool InputDevice::isTouchpad() const
0121 {
0122     return false;
0123 }
0124 
0125 ButtonRebindsFilter::ButtonRebindsFilter()
0126     : m_configWatcher(KConfigWatcher::create(KSharedConfig::openConfig("kcminputrc")))
0127 {
0128     KWin::input()->addInputDevice(&m_inputDevice);
0129     const QLatin1String groupName("ButtonRebinds");
0130     connect(m_configWatcher.get(), &KConfigWatcher::configChanged, this, [this, groupName](const KConfigGroup &group) {
0131         if (group.parent().name() == groupName) {
0132             loadConfig(group.parent());
0133         } else if (group.parent().parent().name() == groupName) {
0134             loadConfig(group.parent().parent());
0135         }
0136     });
0137     loadConfig(m_configWatcher->config()->group(groupName));
0138 }
0139 
0140 void ButtonRebindsFilter::loadConfig(const KConfigGroup &group)
0141 {
0142     Q_ASSERT(QLatin1String("ButtonRebinds") == group.name());
0143     KWin::input()->uninstallInputEventFilter(this);
0144     for (auto &action : m_actions) {
0145         action.clear();
0146     }
0147 
0148     bool foundActions = false;
0149     const auto mouseButtonEnum = QMetaEnum::fromType<Qt::MouseButtons>();
0150     const auto mouseGroup = group.group("Mouse");
0151     static constexpr auto maximumQtExtraButton = 24;
0152     for (int i = 1; i <= maximumQtExtraButton; ++i) {
0153         const QByteArray buttonName = QByteArray("ExtraButton") + QByteArray::number(i);
0154         if (mouseGroup.hasKey(buttonName.constData())) {
0155             const auto entry = mouseGroup.readEntry(buttonName.constData(), QStringList());
0156             const auto button = static_cast<quint32>(mouseButtonEnum.keyToValue(buttonName));
0157             insert(Pointer, {QString(), button}, entry);
0158             foundActions = true;
0159         }
0160     }
0161 
0162     const auto tabletsGroup = group.group("Tablet");
0163     const auto tablets = tabletsGroup.groupList();
0164     for (const auto &tabletName : tablets) {
0165         const auto tabletGroup = tabletsGroup.group(tabletName);
0166         const auto tabletButtons = tabletGroup.keyList();
0167         for (const auto &buttonName : tabletButtons) {
0168             const auto entry = tabletGroup.readEntry(buttonName, QStringList());
0169             bool ok = false;
0170             const uint button = buttonName.toUInt(&ok);
0171             if (ok) {
0172                 foundActions = true;
0173                 insert(TabletPad, {tabletName, button}, entry);
0174             }
0175         }
0176     }
0177 
0178     const auto tabletToolsGroup = group.group("TabletTool");
0179     const auto tabletTools = tabletToolsGroup.groupList();
0180     for (const auto &tabletToolName : tabletTools) {
0181         const auto toolGroup = tabletToolsGroup.group(tabletToolName);
0182         const auto tabletToolButtons = toolGroup.keyList();
0183         for (const auto &buttonName : tabletToolButtons) {
0184             const auto entry = toolGroup.readEntry(buttonName, QStringList());
0185             bool ok = false;
0186             const uint button = buttonName.toUInt(&ok);
0187             if (ok) {
0188                 foundActions = true;
0189                 insert(TabletToolButtonType, {tabletToolName, button}, entry);
0190             }
0191         }
0192     }
0193 
0194     if (foundActions) {
0195         KWin::input()->prependInputEventFilter(this);
0196     }
0197 }
0198 
0199 bool ButtonRebindsFilter::pointerEvent(KWin::MouseEvent *event, quint32 nativeButton)
0200 {
0201     if (event->type() != QEvent::MouseButtonPress && event->type() != QEvent::MouseButtonRelease) {
0202         return false;
0203     }
0204     if (RebindScope::isRebinding()) {
0205         return false;
0206     }
0207 
0208     return send(Pointer, {{}, event->button()}, event->type() == QEvent::MouseButtonPress, event->timestamp());
0209 }
0210 
0211 bool ButtonRebindsFilter::tabletPadButtonEvent(uint button, bool pressed, const KWin::TabletPadId &tabletPadId, std::chrono::microseconds time)
0212 {
0213     if (RebindScope::isRebinding()) {
0214         return false;
0215     }
0216     return send(TabletPad, {tabletPadId.name, button}, pressed, time);
0217 }
0218 
0219 bool ButtonRebindsFilter::tabletToolButtonEvent(uint button, bool pressed, const KWin::TabletToolId &tabletToolId, std::chrono::microseconds time)
0220 {
0221     if (RebindScope::isRebinding()) {
0222         return false;
0223     }
0224     m_tabletTool = tabletToolId;
0225     return send(TabletToolButtonType, {tabletToolId.m_name, button}, pressed, time);
0226 }
0227 
0228 void ButtonRebindsFilter::insert(TriggerType type, const Trigger &trigger, const QStringList &entry)
0229 {
0230     if (entry.size() != 2) {
0231         qCWarning(KWIN_BUTTONREBINDS) << "Failed to rebind to" << entry;
0232         return;
0233     }
0234     if (entry.first() == QLatin1String("Key")) {
0235         const auto keys = QKeySequence::fromString(entry.at(1), QKeySequence::PortableText);
0236         if (!keys.isEmpty()) {
0237             m_actions.at(type).insert(trigger, keys);
0238         }
0239     } else if (entry.first() == QLatin1String("MouseButton")) {
0240         bool ok = false;
0241         const MouseButton mb{entry.last().toUInt(&ok)};
0242         if (ok) {
0243             m_actions.at(type).insert(trigger, mb);
0244         } else {
0245             qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << entry << "into a mouse button";
0246         }
0247     } else if (entry.first() == QLatin1String("TabletToolButton")) {
0248         bool ok = false;
0249         const TabletToolButton tb{entry.last().toUInt(&ok)};
0250         if (ok) {
0251             m_actions.at(type).insert(trigger, tb);
0252         } else {
0253             qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << entry << "into a mouse button";
0254         }
0255     }
0256 }
0257 
0258 bool ButtonRebindsFilter::send(TriggerType type, const Trigger &trigger, bool pressed, std::chrono::microseconds timestamp)
0259 {
0260     const auto &typeActions = m_actions.at(type);
0261     if (typeActions.isEmpty()) {
0262         return false;
0263     }
0264 
0265     const auto &action = typeActions[trigger];
0266     if (const QKeySequence *seq = std::get_if<QKeySequence>(&action)) {
0267         return sendKeySequence(*seq, pressed, timestamp);
0268     }
0269     if (const auto mb = std::get_if<MouseButton>(&action)) {
0270         return sendMouseButton(mb->button, pressed, timestamp);
0271     }
0272     if (const auto tb = std::get_if<TabletToolButton>(&action)) {
0273         return sendTabletToolButton(tb->button, pressed, timestamp);
0274     }
0275     return false;
0276 }
0277 
0278 bool ButtonRebindsFilter::sendKeySequence(const QKeySequence &keys, bool pressed, std::chrono::microseconds time)
0279 {
0280     if (keys.isEmpty()) {
0281         return false;
0282     }
0283     const auto &key = keys[0];
0284 
0285     int sym = -1;
0286     if (!KKeyServer::keyQtToSymX(keys[0], &sym)) {
0287         qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << keys << "to keysym";
0288         return false;
0289     }
0290     // KKeyServer returns upper case syms, lower it to not confuse modifiers handling
0291     auto keyCode = KWin::input()->keyboard()->xkb()->keycodeFromKeysym(sym);
0292     if (!keyCode) {
0293         qCWarning(KWIN_BUTTONREBINDS) << "Could not convert" << keys << "sym: " << sym << "to keycode";
0294         return false;
0295     }
0296 
0297     RebindScope scope;
0298     auto sendKey = [this, pressed, time](xkb_keycode_t key) {
0299         auto state = pressed ? KWin::InputRedirection::KeyboardKeyPressed : KWin::InputRedirection::KeyboardKeyReleased;
0300         Q_EMIT m_inputDevice.keyChanged(key, state, time, &m_inputDevice);
0301     };
0302 
0303     if (key & Qt::ShiftModifier) {
0304         sendKey(KEY_LEFTSHIFT);
0305     }
0306     if (key & Qt::ControlModifier) {
0307         sendKey(KEY_LEFTCTRL);
0308     }
0309     if (key & Qt::AltModifier) {
0310         sendKey(KEY_LEFTALT);
0311     }
0312     if (key & Qt::MetaModifier) {
0313         sendKey(KEY_LEFTMETA);
0314     }
0315 
0316     sendKey(keyCode.value());
0317     return true;
0318 }
0319 
0320 bool ButtonRebindsFilter::sendMouseButton(quint32 button, bool pressed, std::chrono::microseconds time)
0321 {
0322     RebindScope scope;
0323     Q_EMIT m_inputDevice.pointerButtonChanged(button, KWin::InputRedirection::PointerButtonState(pressed), time, &m_inputDevice);
0324     return true;
0325 }
0326 
0327 bool ButtonRebindsFilter::sendTabletToolButton(quint32 button, bool pressed, std::chrono::microseconds time)
0328 {
0329     if (!m_tabletTool) {
0330         return false;
0331     }
0332     RebindScope scope;
0333     Q_EMIT m_inputDevice.tabletToolButtonEvent(button, pressed, *m_tabletTool, time);
0334     return true;
0335 }