File indexing completed on 2024-11-10 04:56:57

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