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 }