diff --git a/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h b/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h index 28973d8..c28260c 100644 --- a/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h +++ b/src/plugin-qt/shortcut/src/backend/abstractkeyhandler.h @@ -46,6 +46,8 @@ class AbstractKeyHandler : public QObject signals: void keyActivated(const QString &shortcutId); + void numLockStateChanged(bool on); + void capsLockStateChanged(bool on); void captureStarted(); void captureKeyEvent(bool pressed, const QString &keystroke); void captureFinished(); diff --git a/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp b/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp index 83b3e4e..ee0cae8 100644 --- a/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp +++ b/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.cpp @@ -26,10 +26,12 @@ #include #include #include +#include #include #include #include +#include // Need to define XK_MISCELLANY before including keysymdef.h to get Caps_Lock, Num_Lock etc. #define XK_MISCELLANY @@ -164,6 +166,7 @@ X11KeyHandler::X11KeyHandler(QObject *parent) } refreshModifierMasks(); enableDetectableAutoRepeat(); + enableLockStateMonitoring(); m_releaseTimer->setSingleShot(true); m_releaseTimer->setInterval(0); @@ -647,6 +650,11 @@ void X11KeyHandler::handleXcbEvents() while ((event = xcb_poll_for_event(m_connection))) { uint8_t responseType = event->response_type & ~0x80; + if (m_xkbEventBase >= 0 + && responseType == static_cast(m_xkbEventBase)) { + notifyLockStateChange(event); + } + if (m_capture.active && responseType == XCB_KEY_PRESS) { const CapturedKey captured = captureKey( reinterpret_cast(event)); @@ -896,6 +904,29 @@ void X11KeyHandler::enableDetectableAutoRepeat() qCWarning(logShortcut) << "X11KeyHandler: falling back to Release/Press autorepeat detection"; } +void X11KeyHandler::enableLockStateMonitoring() +{ + int opcode = 0; + int errorBase = 0; + int major = XkbMajorVersion; + int minor = XkbMinorVersion; + if (!m_display || !XkbQueryExtension(m_display, &opcode, &m_xkbEventBase, + &errorBase, &major, &minor)) { + m_xkbEventBase = -1; + qCWarning(logShortcut) << "X11KeyHandler: XKB extension is unavailable;" + " lock state changes cannot be monitored"; + return; + } + + if (!XkbSelectEventDetails(m_display, XkbUseCoreKbd, XkbStateNotify, + XkbModifierLockMask, XkbModifierLockMask)) { + m_xkbEventBase = -1; + qCWarning(logShortcut) << "X11KeyHandler: Failed to subscribe to XKB lock state changes"; + return; + } + XFlush(m_display); +} + void X11KeyHandler::handleKeyPress(const xcb_key_press_event_t *event) { const auto pending = m_pendingReleases.constFind(event->detail); @@ -937,6 +968,23 @@ void X11KeyHandler::handleKeyPress(const xcb_key_press_event_t *event) activate(shortcutId, KeyEventFlag::Press); } +void X11KeyHandler::notifyLockStateChange(const xcb_generic_event_t *event) +{ + if (!event) + return; + + static_assert(sizeof(xkbStateNotify) == sz_xkbStateNotify); + xkbStateNotify stateEvent{}; + std::memcpy(&stateEvent, event, sizeof(stateEvent)); + if (stateEvent.xkbType != XkbStateNotify + || !(stateEvent.changed & XkbModifierLockMask)) { + return; + } + + emit numLockStateChanged(stateEvent.lockedMods & m_numLockMask); + emit capsLockStateChanged(stateEvent.lockedMods & m_capsLockMask); +} + void X11KeyHandler::handleKeyRelease(const xcb_key_release_event_t *event) { if (!m_pressedBindings.contains(event->detail)) diff --git a/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h b/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h index b7370a5..ce53582 100644 --- a/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h +++ b/src/plugin-qt/shortcut/src/backend/x11/x11keyhandler.h @@ -87,8 +87,10 @@ private slots: QList ignoredModifierCombinations() const; void refreshModifierMasks(); void enableDetectableAutoRepeat(); + void enableLockStateMonitoring(); void handleKeyPress(const xcb_key_press_event_t *event); void handleKeyRelease(const xcb_key_release_event_t *event); + void notifyLockStateChange(const xcb_generic_event_t *event); CapturedKey captureKey(const xcb_key_press_event_t *event) const; bool isCapturedKeyValid(const CapturedKey &key) const; bool hasAnyMask(uint16_t state, const QList &masks) const; @@ -107,6 +109,7 @@ private slots: QList m_rootWindows; xcb_key_symbols_t *m_keySymbols = nullptr; QSocketNotifier *m_notifier = nullptr; + int m_xkbEventBase = -1; // Standalone modifier shortcut monitoring. ModifierKeyMonitor *m_modifierMonitor = nullptr; diff --git a/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp b/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp index 2b0b8a7..050907b 100644 --- a/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp +++ b/src/plugin-qt/shortcut/src/core/keybindingmanager.cpp @@ -302,6 +302,12 @@ KeybindingManager::KeybindingManager(ConfigLoader *loader, ActionExecutor *execu // Connect signals from key handler connect(m_keyHandler, &AbstractKeyHandler::keyActivated, this, &KeybindingManager::onKeyActivated); + connect(m_keyHandler, &AbstractKeyHandler::numLockStateChanged, + this, &KeybindingManager::updateNumLockState); + connect(m_keyHandler, &AbstractKeyHandler::capsLockStateChanged, + this, &KeybindingManager::updateCapsLockState); + m_lastNumLockState = GetNumLockState(); + m_lastCapsLockState = GetCapsLockState(); if (!m_isWayland) { connect(m_keyHandler, &AbstractKeyHandler::captureStarted, this, [this] { m_specialKeyHandler->setEnabled(false); }); @@ -1279,6 +1285,26 @@ void KeybindingManager::onCaptureKeyEvent(bool pressed, const QString &keystroke emit KeyEvent(pressed, keystroke); } +void KeybindingManager::updateNumLockState(bool on) +{ + const uint state = on ? 1U : 0U; + if (m_lastNumLockState == state) + return; + + m_lastNumLockState = state; + emit NumLockStateChanged(state); +} + +void KeybindingManager::updateCapsLockState(bool on) +{ + const uint state = on ? 1U : 0U; + if (m_lastCapsLockState == state) + return; + + m_lastCapsLockState = state; + emit CapsLockStateChanged(state); +} + bool KeybindingManager::registerShortcut(const KeyConfig &config, const QStringList &excludeIds) { if (!config.canRegister()) { @@ -1515,7 +1541,7 @@ void KeybindingManager::SetNumLockState(uint state) uint oldState = GetNumLockState(); m_keyHandler->setNumLockState(state != 0); if (oldState != state) { - emit NumLockStateChanged(state); + updateNumLockState(state != 0); } } } @@ -1526,7 +1552,7 @@ void KeybindingManager::SetCapsLockState(uint state) uint oldState = GetCapsLockState(); m_keyHandler->setCapsLockState(state != 0); if (oldState != state) { - emit CapsLockStateChanged(state); + updateCapsLockState(state != 0); } } } diff --git a/src/plugin-qt/shortcut/src/core/keybindingmanager.h b/src/plugin-qt/shortcut/src/core/keybindingmanager.h index 33e5f11..beb732f 100644 --- a/src/plugin-qt/shortcut/src/core/keybindingmanager.h +++ b/src/plugin-qt/shortcut/src/core/keybindingmanager.h @@ -124,6 +124,8 @@ private slots: void onConfigRemoved(const QString &id); void onKeyActivated(const QString &shortcutId); void onCaptureKeyEvent(bool pressed, const QString &keystroke); + void updateNumLockState(bool on); + void updateCapsLockState(bool on); void onBackendKeymapAboutToChange(); void onBackendKeymapChanged(); ShortcutInfo toShortcutInfo(const KeyConfig &config); @@ -181,6 +183,8 @@ private slots: QMap m_keyConfigsMap; QSet m_activeShortcutIds; QSet m_resetInProgressIds; + uint m_lastNumLockState = 0; + uint m_lastCapsLockState = 0; bool m_isWayland = false; };