From be41a7b839233462dc8fbd2ac092cf4a1efdb491 Mon Sep 17 00:00:00 2001 From: liujiangtao Date: Tue, 13 Oct 2026 16:34:02 +0800 Subject: [PATCH] fix(shortcut): exclude consumed keys from wl_keyboard.enter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A key matching a registered shortcut is consumed by shortcut handling and never reaches the client. Exclude such keys from wl_keyboard.enter so the client never learns the key was held, and clear its record once the release arrives, leaving no stale entry behind. 命中已注册快捷键的按键会被快捷键处理消费,不会下发给客户端。 因此 enter 的按键集合需剔除这类按键,客户端不会误判该键按下; 待其 release 到达时再清理对应记录,确保无残留。 Log: 修复 Alt+Tab 切换后客户端持续收到 Tab 的问题 Influence: Alt+Tab 等快捷键切换焦点时,客户端不再从 enter 得知被快捷键消费的 按键;该按键的 release 到达后记录随即清理,不会残留影响后续按键。 PMS: BUG-379569 --- src/modules/shortcut/shortcutcontroller.cpp | 7 +++- src/modules/shortcut/shortcutcontroller.h | 2 + src/seat/helper.cpp | 22 ++++++++++- waylib/src/server/kernel/wseat.cpp | 42 ++++++++++++++++++++- waylib/src/server/kernel/wseat.h | 12 ++++++ 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/src/modules/shortcut/shortcutcontroller.cpp b/src/modules/shortcut/shortcutcontroller.cpp index f94d31c969..eb0b45bece 100644 --- a/src/modules/shortcut/shortcutcontroller.cpp +++ b/src/modules/shortcut/shortcutcontroller.cpp @@ -218,6 +218,11 @@ void ShortcutController::unregisterShortcut(const QString &name) } } +bool ShortcutController::matchesShortcut(const QKeyEvent *event) const +{ + return m_keyMap.contains(normalizeKeyCombination(event->keyCombination()).toCombined()); +} + bool ShortcutController::dispatchKeyEvent(const QKeyEvent *kevent) { auto combined = normalizeKeyCombination(kevent->keyCombination()).toCombined(); @@ -225,7 +230,7 @@ bool ShortcutController::dispatchKeyEvent(const QKeyEvent *kevent) | (kevent->type() == QEvent::KeyPress ? ShortcutController::KeyPress : ShortcutController::None) | (kevent->type() == QEvent::KeyRelease ? ShortcutController::KeyRelease : ShortcutController::None); - bool matched = m_keyMap.contains(combined); + bool matched = matchesShortcut(kevent); if (matched) { qCInfo(lcTlShortcut).noquote() << "shortcut match:" << QKeySequence(kevent->keyCombination()).toString(QKeySequence::PortableText) diff --git a/src/modules/shortcut/shortcutcontroller.h b/src/modules/shortcut/shortcutcontroller.h index 456d68d98d..c41afed0af 100644 --- a/src/modules/shortcut/shortcutcontroller.h +++ b/src/modules/shortcut/shortcutcontroller.h @@ -67,6 +67,8 @@ class ShortcutController : public QObject void clear(); bool dispatchKeyEvent(const QKeyEvent *event); + + bool matchesShortcut(const QKeyEvent *event) const; static QKeyCombination normalizeKeyCombination(QKeyCombination combination); static bool isValidShortcutCombination(QKeyCombination combination); Qt::KeyboardModifiers modifierForAction(ShortcutAction action) const; diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 58e7330f7b..c6cf425238 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -1527,6 +1527,17 @@ bool Helper::beforeDisposeEvent(WSeat *seat, QWindow *targetWindow, QInputEvent // Dismiss the popup grab when the user presses a button outside the popup // (e.g. on the desktop or another client). wlroots' xdg popup keyboard grab // redirects keyboard focus back to the popup, so it must be ended explicitly. + + // Clear the record on release before the early returns below can bypass the + // tracked block, otherwise the entry would linger until WSeat::destroy(). + // Auto-repeats are skipped: only the real release ends the press. + if (seat == m_primarySeat && event->type() == QEvent::KeyRelease) { + const auto *kevent = static_cast(event); + const uint32_t evdevKeycode = kevent->nativeVirtualKey(); + if (!kevent->isAutoRepeat() && seat->isKeyFiltered(evdevKeycode)) + seat->removeFilteredKey(evdevKeycode); + } + if (event->type() == QEvent::MouseButtonPress) { if (auto *seatContainer = m_rootSurfaceContainer->getSeatContainer(targetSeat)) { if (seatContainer->hasPopupGrab()) { @@ -1671,9 +1682,16 @@ bool Helper::beforeDisposeEvent(WSeat *seat, QWindow *targetWindow, QInputEvent } } - if (m_shortcutManager->controller()->dispatchKeyEvent(kevent)) { + auto *controller = m_shortcutManager->controller(); + const uint32_t evdevKeycode = kevent->nativeVirtualKey(); + const bool isAutoRepeat = kevent->isAutoRepeat(); + + if (!isAutoRepeat && kevent->type() == QEvent::KeyPress + && controller->matchesShortcut(kevent)) + seat->addFilteredKey(evdevKeycode); + + if (controller->dispatchKeyEvent(kevent)) return true; - } } return false; diff --git a/waylib/src/server/kernel/wseat.cpp b/waylib/src/server/kernel/wseat.cpp index 48b90d7d9c..cc6513d35d 100644 --- a/waylib/src/server/kernel/wseat.cpp +++ b/waylib/src/server/kernel/wseat.cpp @@ -236,7 +236,12 @@ class Q_DECL_HIDDEN WSeatPrivate : public WObjectPrivate // Send keyboard enter with current modifiers. // This ensures the newly focused client receives the current modifier state // (Num Lock, Caps Lock, etc.) as required by Wayland protocol. - wlr_seat_keyboard_notify_enter(handle(), surface, keycodes, numKeycodes, modifiers); + // Keys consumed by shortcut handling are dropped. + uint32_t visibleKeycodes[WLR_KEYBOARD_KEYS_CAP]; + const size_t numVisibleKeycodes = + WSeat::unfilteredKeycodes(keycodes, numKeycodes, filteredKeys, + visibleKeycodes); + wlr_seat_keyboard_notify_enter(handle(), surface, visibleKeycodes, numVisibleKeycodes, modifiers); } else { // Grab-aware: the raw wlr_seat_keyboard_clear_focus() would // bypass an active keyboard grab (IME, popup, drag). @@ -386,6 +391,9 @@ class Q_DECL_HIDDEN WSeatPrivate : public WObjectPrivate QPointer focusWindow; QPointer pointerFocusEventObject; QPointer m_keyboardFocusSurface; + // Evdev keycodes whose press was consumed by shortcut handling; + // see WSeat::addFilteredKey(). + QSet filteredKeys; QMetaObject::Connection onEventObjectDestroy; wlr_surface *oldPointerFocusSurface = nullptr; @@ -1186,6 +1194,36 @@ void WSeat::clearKeyboardFocusSurface() d->doSetKeyboardFocus(nullptr); } +void WSeat::addFilteredKey(uint32_t keycode) +{ + W_D(WSeat); + d->filteredKeys.insert(keycode); +} + +void WSeat::removeFilteredKey(uint32_t keycode) +{ + W_D(WSeat); + d->filteredKeys.remove(keycode); +} + +bool WSeat::isKeyFiltered(uint32_t keycode) const +{ + W_DC(WSeat); + return d->filteredKeys.contains(keycode); +} + +size_t WSeat::unfilteredKeycodes(const uint32_t *keycodes, size_t numKeycodes, + const QSet &filteredKeys, uint32_t *out) +{ + size_t numUnfiltered = 0; + for (size_t i = 0; i < numKeycodes; ++i) { + if (filteredKeys.contains(keycodes[i])) + continue; + out[numUnfiltered++] = keycodes[i]; + } + return numUnfiltered; +} + void WSeat::setKeyboardFocusWindow(QWindow *window) { W_D(WSeat); @@ -1679,6 +1717,8 @@ void WSeat::destroy(WServer *) d->deviceList.clear(); + d->filteredKeys.clear(); + // Need not call the DCursor::detachInputDevice on destroy WSeat, so do // call the detachCursor at clear the deviceList after. if (d->cursor) diff --git a/waylib/src/server/kernel/wseat.h b/waylib/src/server/kernel/wseat.h index 5cf5308fb2..6b5853548e 100644 --- a/waylib/src/server/kernel/wseat.h +++ b/waylib/src/server/kernel/wseat.h @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -93,6 +94,17 @@ class WAYLIB_SERVER_EXPORT WSeat : public QObject, public WObject, public WServe WSurface *keyboardFocusSurface() const; void clearKeyboardFocusSurface(); + void addFilteredKey(uint32_t keycode); + void removeFilteredKey(uint32_t keycode); + bool isKeyFiltered(uint32_t keycode) const; + + // Copies the keycodes the client is allowed to see into `out`, dropping the + // filtered ones, and returns the number of copied keycodes. `out` must be + // able to hold `numKeycodes` elements. `keycodes` may be null when + // `numKeycodes` is 0. + static size_t unfilteredKeycodes(const uint32_t *keycodes, size_t numKeycodes, + const QSet &filteredKeys, uint32_t *out); + void setKeyboardFocusWindow(QWindow *window); QWindow *keyboardFocusWindow() const; void clearKeyboardFocusWindow();