From 45e4d6a3fa132f78153bec5fb5cebd02b26ad203 Mon Sep 17 00:00:00 2001 From: JiDe Zhang Date: Mon, 21 Sep 2026 10:22:35 +0800 Subject: [PATCH] fix(xwayland): sync keyboard modifiers state to XWayland MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Listen to the seat keyboard modifiers event and proactively push wl_keyboard.modifiers to the XWayland server client when XWayland itself does not hold the keyboard focus. 2. Handle keyboard device switch/destroy and the timing where the seat is set before the XWayland server starts (re-sync on ready). 3. Allow setSeat to accept a null seat. X11 APIs that query keyboard state (e.g. XkbGetState, Qt QGuiApplication::keyboardModifiers) now reflect the current modifier state even when the keyboard focus is on a Wayland client. Log: XWayland 应用通过 X11 接口查询到的修饰键状态(Caps/Num Lock 等)不再过期。 Influence: 1. Run an XWayland app, press Caps Lock/Num Lock while focus is on a Wayland window, then check the state via X11 query APIs. 2. Switch the input method virtual keyboard on/off and verify modifier state stays correct. 3. Verify no regression in normal keyboard focus flow on XWayland apps. fix(xwayland): 同步键盘修饰键状态到 XWayland 1. 监听 seat 键盘修饰键事件,在 XWayland 自身不持有键盘焦点时 主动向其推送 wl_keyboard.modifiers。 2. 处理键盘设备切换/销毁、seat 先于 XWayland 启动的时序。 3. setSeat 支持传入空 seat。 Log: XWayland 应用通过 X11 接口查询到的修饰键状态不再过期。 Influence: 1. 在 Wayland 窗口上按 Caps/Num Lock,切换到 XWayland 应用后用 X11 接口验证锁定状态。 2. 开关输入法虚拟键盘,验证修饰键状态保持正确。 3. 验证 XWayland 应用正常键盘焦点流程无回归。 --- waylib/src/server/protocols/wxwayland.cpp | 101 +++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/waylib/src/server/protocols/wxwayland.cpp b/waylib/src/server/protocols/wxwayland.cpp index 9bf800f5b..3a881b47b 100644 --- a/waylib/src/server/protocols/wxwayland.cpp +++ b/waylib/src/server/protocols/wxwayland.cpp @@ -20,6 +20,9 @@ #include #include +// For wl_keyboard_send_modifiers +#include + #include #include @@ -59,6 +62,15 @@ class Q_DECL_HIDDEN WXWaylandPrivate : public WWaylandResourcePrivate QMetaObject::Connection cursorPositionChangedConnection; + // Keyboard modifiers sync to the Xwayland server. + void syncModifiersToXWayland(); + void watchSeatKeyboard(); + void on_keyboard_device_destroy(); + + WScopedListener keyboardModifiersListener; + WScopedListener keyboardDeviceDestroyListener; + QMetaObject::Connection seatKeyboardChangedConnection; + // Async property reading struct PerWindowProps { @@ -485,6 +497,89 @@ void WXWaylandPrivate::watchSeatCursor() } } +void WXWaylandPrivate::syncModifiersToXWayland() +{ + W_Q(WXWayland); + + auto *xwayland = q->handle(); + if (!xwayland || !xwayland->server || !xwayland->server->client || !xwayland->seat) + return; + + auto *seat = xwayland->seat; + auto *keyboard = wlr_seat_get_keyboard(seat); + if (!keyboard) + return; + + // When Xwayland itself holds the keyboard focus, the seat already + // delivers the modifiers to it through the normal path. + auto *focusedClient = seat->keyboard_state.focused_client; + if (focusedClient && focusedClient->client == xwayland->server->client) + return; + + auto *seatClient = wlr_seat_client_for_wl_client(seat, xwayland->server->client); + if (!seatClient || wl_list_empty(&seatClient->keyboards)) + return; + + // The Xwayland server applies every wl_keyboard.modifiers event to its + // XKB state (keyboard_handle_modifiers in xwayland-input.c) even when it + // doesn't hold the keyboard focus, so push the current state explicitly + // to keep the state seen by X11 APIs (e.g. XkbGetState) fresh. + const auto &mods = keyboard->modifiers; + uint32_t serial = wlr_seat_client_next_serial(seatClient); + wl_resource *resource; + wl_resource_for_each(resource, &seatClient->keyboards) { + wl_keyboard_send_modifiers(resource, serial, + mods.depressed, mods.latched, + mods.locked, mods.group); + } +} + +void WXWaylandPrivate::watchSeatKeyboard() +{ + W_Q(WXWayland); + + keyboardModifiersListener.disconnect(); + keyboardDeviceDestroyListener.disconnect(); + + // setSeat() may switch to another seat (or clear it), so rebind the + // keyboardChanged connection to the current seat on every call. + if (seatKeyboardChangedConnection) { + QObject::disconnect(seatKeyboardChangedConnection); + seatKeyboardChangedConnection = {}; + } + + auto *xwayland = q->handle(); + auto *seat = xwayland ? xwayland->seat : nullptr; + if (!seat) + return; + + // The seat keyboard can be replaced (e.g. by the input method's virtual + // keyboard) or destroyed at any time, so re-attach on changes. + if (auto *seatObject = WSeat::fromHandle(seat)) { + seatKeyboardChangedConnection = QObject::connect(seatObject, + &WSeat::keyboardChanged, + q, [this] { + watchSeatKeyboard(); + syncModifiersToXWayland(); + }); + } + + if (auto *keyboard = wlr_seat_get_keyboard(seat)) { + keyboardModifiersListener.init(&keyboard->events.modifiers, + this, &WXWaylandPrivate::syncModifiersToXWayland); + keyboardDeviceDestroyListener.init(&keyboard->base.events.destroy, + this, &WXWaylandPrivate::on_keyboard_device_destroy); + } +} + +void WXWaylandPrivate::on_keyboard_device_destroy() +{ + // The keyboard is going away; detach our listeners from it before the + // wlr_keyboard memory is freed. + keyboardModifiersListener.disconnect(); + keyboardDeviceDestroyListener.disconnect(); +} + void WXWayland::setSeat(WSeat *seat) { W_D(WXWayland); @@ -494,6 +589,8 @@ void WXWayland::setSeat(WSeat *seat) d->watchSeatCursor(); d->syncCursorPositionToXWayland(); + d->watchSeatKeyboard(); + d->syncModifiersToXWayland(); } WSeat *WXWayland::seat() const @@ -612,9 +709,11 @@ void WXWayland::create(WServer *server) listeners()->add(&handle->events.ready, this, [this, d] (void *) { d->init(); // The seat may be set before the Xwayland server starts; sync the - // current cursor position once the server client is available. + // current cursor position and keyboard state once the server client + // is available. d->watchSeatCursor(); d->syncCursorPositionToXWayland(); + d->syncModifiersToXWayland(); Q_EMIT ready(); });