diff --git a/3rdparty/wlroots/xwayland/xwm.c b/3rdparty/wlroots/xwayland/xwm.c index f31163d943..f638dc25f6 100644 --- a/3rdparty/wlroots/xwayland/xwm.c +++ b/3rdparty/wlroots/xwayland/xwm.c @@ -1915,6 +1915,48 @@ static bool validate_focus_serial(uint16_t last_focus_seq, uint16_t event_seq) { return true; } +// Returns true if `child` is located inside `parent`'s subtree (not `parent` +// itself), walking at most `max_depth` levels up from `child`. Used to tell +// apart in-application focus moves into XEmbed plug windows (which a real X11 +// window manager never interferes with) from true cross-application focus +// stealing. Walking up from `child` needs at most `max_depth` synchronous X +// queries, while a top-down search has to visit every window of `parent`'s +// subtree (unbounded in width) before it can answer "no" -- which is the +// common case for cross-application focus changes. +static bool window_is_descendant(struct wlr_xwm *xwm, xcb_window_t parent, + xcb_window_t child, unsigned int max_depth) { + xcb_window_t window = child; + unsigned int queries = 0; + bool found = false; + + while (window != parent && queries < max_depth) { + xcb_query_tree_cookie_t cookie = + xcb_query_tree(xwm->xcb_conn, window); + xcb_query_tree_reply_t *reply = + xcb_query_tree_reply(xwm->xcb_conn, cookie, NULL); + if (reply == NULL) { + break; + } + xcb_window_t ancestor = reply->parent; + free(reply); + queries++; + + if (ancestor == parent) { + found = true; + break; + } + if (ancestor == XCB_NONE || ancestor == xwm->screen->root) { + break; + } + window = ancestor; + } + + wlr_log(WLR_DEBUG, "focus descendant check: child=0x%" PRIx32 + " parent=0x%" PRIx32 " queries=%u result=%d", + child, parent, queries, found); + return found; +} + static void xwm_handle_focus_in(struct wlr_xwm *xwm, xcb_focus_in_event_t *ev) { // Ignore pointer focus change events @@ -1951,6 +1993,16 @@ static void xwm_handle_focus_in(struct wlr_xwm *xwm, (xwm->offered_focus && xsurface->pid == xwm->offered_focus->pid))) { xwm_set_focused_window(xwm, xsurface); wl_signal_emit_mutable(&xsurface->events.focus_in, NULL); + } else if (xwm->focus_surface && ev->event != XCB_NONE && + window_is_descendant(xwm, xwm->focus_surface->window_id, + ev->event, 8)) { + // The newly focused window is a subwindow of the currently focused + // surface's hierarchy: an in-client focus move (e.g. XEmbed plug + // windows used by WPS Office and other embedded-toolkit apps). + // A real X11 window manager leaves these alone; refocusing here + // would strip input focus from the embedder's plug and break + // anything inside that window gated on real X focus (notably + // input-method activation via XIM). } else { // Try to prevent clients from changing focus between // applications, by refocusing the previous surface. diff --git a/misc/systemd/dde-session-pre.target.wants/treeland-xwayland.service.in b/misc/systemd/dde-session-pre.target.wants/treeland-xwayland.service.in index c84e4e4132..a0af31bc7d 100644 --- a/misc/systemd/dde-session-pre.target.wants/treeland-xwayland.service.in +++ b/misc/systemd/dde-session-pre.target.wants/treeland-xwayland.service.in @@ -17,8 +17,9 @@ ExecCondition=/bin/sh -c 'test "$XDG_SESSION_DESKTOP" = "Treeland" || exit 2' Type=notify Sockets=treeland-xwayland.socket UnsetEnvironment=DISPLAY +UnsetEnvironment=XAUTHORITY ExecStart=@CMAKE_INSTALL_FULL_LIBEXECDIR@/treeland-sd --type xwayland -ExecStop=-/usr/bin/systemctl --user unset-environment DISPLAY +ExecStop=-/usr/bin/systemctl --user unset-environment DISPLAY XAUTHORITY Restart=on-failure RestartSec=3s Slice=session.slice diff --git a/src/core/shellhandler.cpp b/src/core/shellhandler.cpp index 5114af2001..1be0a87d6b 100644 --- a/src/core/shellhandler.cpp +++ b/src/core/shellhandler.cpp @@ -514,6 +514,10 @@ void ShellHandler::init(WServer *server, WSeat *seat) &WInputMethodHelper::inputPopupSurfaceV2Removed, this, &ShellHandler::onInputPopupSurfaceV2Removed); + connect(m_inputMethodHelper, + &WInputMethodHelper::textInputFocusSurfaceChanged, + this, + &ShellHandler::onTextInputFocusSurfaceChanged); auto *overlay = QQuickOverlay::overlay(m_rootSurfaceContainer->window()); overlay->setZ(RootSurfaceContainer::GlobalOverlayZOrder); @@ -1277,11 +1281,20 @@ void ShellHandler::updateLayerSurfaceContainer(SurfaceWrapper *surface) void ShellHandler::onInputPopupSurfaceV2Added(WInputPopupSurface *surface) { + auto parent = surface->parentSurface(); + auto parentWrapper = m_rootSurfaceContainer->getSurface(parent); + if (!parentWrapper) { + // The anchoring text input surface is gone (client teardown): there is + // nothing to attach the candidate window to. + qCWarning(lcTlShell) << "[popup] skip input popup without a parent wrapper" + << "surface=" << surface + << "parent=" << parent; + return; + } + auto wrapper = new SurfaceWrapper(Helper::instance()->qmlEngine(), surface, SurfaceWrapper::Type::InputPopup); - auto parent = surface->parentSurface(); - auto parentWrapper = m_rootSurfaceContainer->getSurface(parent); parentWrapper->addSubSurface(wrapper); m_popupContainer->addSurface(wrapper); // m_popupContainer is a simple SurfaceContainer, so input popups need the @@ -1289,17 +1302,36 @@ void ShellHandler::onInputPopupSurfaceV2Added(WInputPopupSurface *surface) wrapper->setHasInitializeContainer(true); wrapper->setOwnsOutput(parentWrapper->ownsOutput()); Q_ASSERT(wrapper->parentItem()); + m_inputPopupWrappers.append(wrapper); Q_EMIT surfaceWrapperAdded(wrapper); } void ShellHandler::onInputPopupSurfaceV2Removed(WInputPopupSurface *surface) { auto wrapper = m_rootSurfaceContainer->getSurface(surface->surface()); + if (!wrapper) { + // onInputPopupSurfaceV2Added() skipped this popup (no parent wrapper + // left), so there is no wrapper to detach here either. + return; + } + m_inputPopupWrappers.removeAll(wrapper); Q_EMIT surfaceWrapperAboutToRemove(wrapper); wrapper->setHasInitializeContainer(false); m_rootSurfaceContainer->destroyForSurface(wrapper); } +void ShellHandler::onTextInputFocusSurfaceChanged() +{ + m_inputPopupWrappers.removeIf([](const QPointer &popup) { + return popup.isNull(); + }); + + for (const auto &popup : std::as_const(m_inputPopupWrappers)) { + if (auto *output = popup->ownsOutput()) + output->retargetInputPopupSurface(popup); + } +} + void ShellHandler::setupSurfaceWindowMenu(SurfaceWrapper *wrapper) { Q_ASSERT(m_windowMenu); diff --git a/src/core/shellhandler.h b/src/core/shellhandler.h index 7230e74995..5e85d66fd0 100644 --- a/src/core/shellhandler.h +++ b/src/core/shellhandler.h @@ -129,6 +129,7 @@ private Q_SLOTS: void setupDockPreview(); void onInputPopupSurfaceV2Removed(WAYLIB_SERVER_NAMESPACE::WInputPopupSurface *surface); + void onTextInputFocusSurfaceChanged(); void onWindowMenuClosed(); private: @@ -196,6 +197,9 @@ private Q_SLOTS: SurfaceContainer *m_privilegedOverlayContainer = nullptr; SurfaceContainer *m_snapMaskContainer = nullptr; IMCandidatePanelManager *m_imCandidatePanelManager = nullptr; + // Input method popup (candidate window) wrappers, re-arranged whenever the + // text input that owns the input method focus moves. + QList> m_inputPopupWrappers; QObject *m_windowMenu = nullptr; // Prelaunch wrappers created before binding to a real shell surface QList m_prelaunchWrappers; diff --git a/src/output/output.cpp b/src/output/output.cpp index bcc6995558..6412a66fbf 100644 --- a/src/output/output.cpp +++ b/src/output/output.cpp @@ -7,6 +7,7 @@ #include "cmdline.h" #include "common/treelandlogging.h" #include "core/rootsurfacecontainer.h" +#include "core/shellhandler.h" #include "core/dconfigmanager.h" #include "outputconfig.hpp" #include "seat/helper.h" @@ -16,6 +17,7 @@ #include "workspace/workspace.h" #include "wallpapermanager.h" +#include #include #include #include @@ -848,7 +850,7 @@ namespace { // - Input popups: cursorRect is in raw surface coordinates; adjust by // content geometry offset (e.g., CSD title bar) to match the rendering // position of WSurfaceItem's contentContainer. -std::optional popupDPos(SurfaceWrapper *surface) +std::optional popupDPos(SurfaceWrapper *surface, SurfaceWrapper *placementParent) { using Type = SurfaceWrapper::Type; if (surface->type() == Type::XdgPopup) { @@ -861,9 +863,8 @@ std::optional popupDPos(SurfaceWrapper *surface) auto *shell = qobject_cast(surface->shellSurface()); if (shell) { QPointF dPos = shell->cursorRect().bottomLeft(); - auto parent = surface->parentSurface(); - if (parent && parent->shellSurface()) { - const QPoint offset = parent->shellSurface()->getContentGeometry().topLeft(); + if (placementParent && placementParent->shellSurface()) { + const QPoint offset = placementParent->shellSurface()->getContentGeometry().topLeft(); dPos -= QPointF(offset.x(), offset.y()); } return dPos; @@ -873,22 +874,45 @@ std::optional popupDPos(SurfaceWrapper *surface) qCWarning(lcTlOutput) << " Invalid popup surface type:" << surface->type(); return std::nullopt; } + +// The input method's panel (candidate window) belongs to the text input that +// currently owns the input method focus. The popup wrapper's own parentSurface() +// only records the surface that happened to be focused when the input method +// created its panel surface, so it goes stale as soon as the same application +// moves the text input focus, e.g. to another of its windows. +SurfaceWrapper *inputPopupPlacementParent(SurfaceWrapper *popup) +{ + auto *helper = Helper::instance(); + if (!helper) + return popup->parentSurface(); + + auto *shellHandler = helper->shellHandler(); + auto *inputMethodHelper = shellHandler ? shellHandler->inputMethodHelper() : nullptr; + auto *rootContainer = helper->rootSurfaceContainer(); + if (inputMethodHelper && rootContainer) { + if (auto *focusSurface = inputMethodHelper->textInputFocusSurface()) { + if (auto *focusWrapper = rootContainer->getSurface(focusSurface)) + return focusWrapper; + } + } + + return popup->parentSurface(); +} } // namespace -QPointF Output::calculateBasePosition(SurfaceWrapper *surface, const QPointF &dPos) const +QPointF Output::calculateBasePosition(SurfaceWrapper *placementParent, const QPointF &dPos) const { - auto parent = surface->parentSurface(); - if (!parent || !parent->surfaceItem()) { + if (!placementParent || !placementParent->surfaceItem()) { qCWarning(lcTlOutput) << " Invalid parent surface or surface item!"; return QPointF(); } - const qreal titlebarOffset = parent->titlebarGeometry().isNull() + const qreal titlebarOffset = placementParent->titlebarGeometry().isNull() ? 0.0 - : parent->titlebarGeometry().height(); + : placementParent->titlebarGeometry().height(); - return QPointF(parent->x() + parent->surfaceItem()->x() + dPos.x(), - parent->y() + parent->surfaceItem()->y() + dPos.y() + titlebarOffset); + return QPointF(placementParent->x() + placementParent->surfaceItem()->x() + dPos.x(), + placementParent->y() + placementParent->surfaceItem()->y() + dPos.y() + titlebarOffset); } void Output::adjustToOutputBounds(QPointF &pos, const QRectF &normalGeo, const QRectF &outputRect) const @@ -908,19 +932,19 @@ void Output::adjustToOutputBounds(QPointF &pos, const QRectF &normalGeo, const Q } } -void Output::handleLayerShellPopup(SurfaceWrapper *surface, const QRectF &normalGeo) +void Output::handleLayerShellPopup(SurfaceWrapper *surface, SurfaceWrapper *placementParent, const QRectF &normalGeo) { - if (!surface->parentSurface() || !surface->parentSurface()->ownsOutput()) { + if (!placementParent || !placementParent->ownsOutput()) { qCWarning(lcTlOutput) << " Invalid LayerShell parent surface!"; return; } - auto parentOutput = surface->parentSurface()->ownsOutput()->outputItem(); - auto dPos = popupDPos(surface); + auto parentOutput = placementParent->ownsOutput()->outputItem(); + auto dPos = popupDPos(surface, placementParent); if (!dPos.has_value()) return; - QPointF pos = calculateBasePosition(surface, dPos.value()); + QPointF pos = calculateBasePosition(placementParent, dPos.value()); if (pos.isNull()) { return; } @@ -930,17 +954,17 @@ void Output::handleLayerShellPopup(SurfaceWrapper *surface, const QRectF &normal surface->moveNormalGeometryInOutput(pos); } -void Output::handleRegularPopup(SurfaceWrapper *surface, const QRectF &normalGeo, WOutputItem *targetOutput) +void Output::handleRegularPopup(SurfaceWrapper *surface, SurfaceWrapper *placementParent, const QRectF &normalGeo, WOutputItem *targetOutput) { if (normalGeo.isEmpty()) { return; } - auto dPos = popupDPos(surface); + auto dPos = popupDPos(surface, placementParent); if (!dPos.has_value()) return; - QPointF pos = calculateBasePosition(surface, dPos.value()); + QPointF pos = calculateBasePosition(placementParent, dPos.value()); if (pos.isNull()) { return; } @@ -976,8 +1000,15 @@ void Output::clearPopupCache(SurfaceWrapper *surface) void Output::arrangePopupSurface(SurfaceWrapper *surface) { - SurfaceWrapper *parentSurfaceWrapper = surface->parentSurface(); - if (!parentSurfaceWrapper) { + SurfaceWrapper *placementParent = surface->parentSurface(); + if (surface->type() == SurfaceWrapper::Type::InputPopup) + placementParent = inputPopupPlacementParent(surface); + arrangePopupSurfaceWith(surface, placementParent); +} + +void Output::arrangePopupSurfaceWith(SurfaceWrapper *surface, SurfaceWrapper *placementParent) +{ + if (!placementParent) { // When an input popup is still alive while its parent text-input client is being torn down, // arrangePopupSurface() can run in a transient state where parentSurface is temporarily unavailable. qCWarning(lcTlSurface) << "[popup] skip arrangePopupSurface: missing parent surface" @@ -996,24 +1027,48 @@ void Output::arrangePopupSurface(SurfaceWrapper *surface) auto *outputAtCursor = Helper::instance()->getOutputAtCursor(); targetOutput = outputAtCursor ? outputAtCursor->outputItem() : nullptr; } else if (surface->isInputPopupLike()) { - auto *parentOutput = parentSurfaceWrapper->ownsOutput(); + auto *parentOutput = placementParent->ownsOutput(); targetOutput = parentOutput ? parentOutput->outputItem() : nullptr; } if (!targetOutput) { qCInfo(lcTlSurface) << "[popup] skip arrangePopupSurface: missing target output" << "surface=" << surface - << "parentSurface=" << parentSurfaceWrapper; + << "parentSurface=" << placementParent; return; } - if (parentSurfaceWrapper->type() == SurfaceWrapper::Type::Layer) { - handleLayerShellPopup(surface, normalGeo); + if (placementParent->type() == SurfaceWrapper::Type::Layer) { + handleLayerShellPopup(surface, placementParent, normalGeo); } else { - handleRegularPopup(surface, normalGeo, targetOutput); + handleRegularPopup(surface, placementParent, normalGeo, targetOutput); } } +void Output::retargetInputPopupSurface(SurfaceWrapper *popup) +{ + if (!popup || popup->type() != SurfaceWrapper::Type::InputPopup) + return; + + auto *placementParent = inputPopupPlacementParent(popup); + if (placementParent) { + if (auto *placementOutput = placementParent->ownsOutput(); + placementOutput && placementOutput != popup->ownsOutput()) { + // Detaches the popup from this output (dropping its cached position) + // and registers it with the new one. + popup->setOwnsOutput(placementOutput); + } + } + + qCDebug(lcTlSurface) << "[popup] retarget input popup" + << "popup=" << popup + << "placementParent=" << placementParent + << "ownsOutput=" << popup->ownsOutput(); + // Reuse the parent resolved above: it walks the root surface container, + // which arrangePopupSurface() would otherwise repeat for input popups. + arrangePopupSurfaceWith(popup, placementParent); +} + void Output::arrangeNonLayerSurfaces(ArrangeReason reason) { for (SurfaceWrapper *surface : std::as_const(surfaces())) { diff --git a/src/output/output.h b/src/output/output.h index 81843a89ba..928d66fab5 100644 --- a/src/output/output.h +++ b/src/output/output.h @@ -106,6 +106,10 @@ class Output : public SurfaceListModel const QRectF &normalGeo, const QRectF &outputRect) const; + // Re-run placement of an input method popup: its anchoring text-input + // surface may have changed (another window of the same application). + void retargetInputPopupSurface(SurfaceWrapper *popup); + Q_SIGNALS: void exclusiveZoneChanged(); void moveResizeFinised(); @@ -153,9 +157,17 @@ public Q_SLOTS: void moveSurfaceWithTitlebarClamp(SurfaceWrapper *surface, const QPointF &pos); qreal preferredScaleFactor() const; - QPointF calculateBasePosition(SurfaceWrapper *surface, const QPointF &dPos) const; - void handleLayerShellPopup(SurfaceWrapper *surface, const QRectF &normalGeo); - void handleRegularPopup(SurfaceWrapper *surface, const QRectF &normalGeo, WOutputItem *targetOutput); + QPointF calculateBasePosition(SurfaceWrapper *placementParent, const QPointF &dPos) const; + // Shared placement body; `placementParent` is the surface the popup is + // positioned relative to and must already be resolved by the caller. + void arrangePopupSurfaceWith(SurfaceWrapper *surface, SurfaceWrapper *placementParent); + void handleLayerShellPopup(SurfaceWrapper *surface, + SurfaceWrapper *placementParent, + const QRectF &normalGeo); + void handleRegularPopup(SurfaceWrapper *surface, + SurfaceWrapper *placementParent, + const QRectF &normalGeo, + WOutputItem *targetOutput); void clearPopupCache(SurfaceWrapper *surface); Type m_type; diff --git a/src/surface/seatsurfacemanager.cpp b/src/surface/seatsurfacemanager.cpp index 19f0208c8d..e9c390f6b2 100644 --- a/src/surface/seatsurfacemanager.cpp +++ b/src/surface/seatsurfacemanager.cpp @@ -8,7 +8,6 @@ #include "common/treelandlogging.h" #include "seat/helper.h" #include "seat/seatmanager.h" -#include "core/shellhandler.h" #include "output/output.h" #include @@ -20,15 +19,40 @@ #include #include -#include - #include #include #include +#include + WAYLIB_SERVER_USE_NAMESPACE +namespace { +wlr_seat_keyboard_grab *keyboardGrabForPopup(wlr_xdg_popup *popup) +{ + if (!popup || !popup->seat || !popup->base || !popup->base->client + || !popup->base->client->shell) { + return nullptr; + } + + auto *shell = popup->base->client->shell; + wlr_xdg_popup_grab *popupGrab; + wl_list_for_each(popupGrab, &shell->popup_grabs, link) { + if (popupGrab->seat != popup->seat) + continue; + + wlr_xdg_popup *member; + wl_list_for_each(member, &popupGrab->popups, grab_link) { + if (member == popup) + return &popupGrab->keyboard_grab; + } + } + + return nullptr; +} +} + SeatSurfaceManager::SeatSurfaceManager(WSeat *seat, RootSurfaceContainer *parent) : QObject(parent) , m_seat(seat) @@ -424,16 +448,35 @@ void SeatSurfaceManager::surfaceDestroyed(SurfaceWrapper *surface) setActivatedSurface(nullptr, Qt::OtherFocusReason); } + const bool trackedPopup = isTrackedPopup(surface); + QPointer restoreTarget; + if (trackedPopup && m_keyboardFocusSurface == surface) { + restoreTarget = popupParentFocusTarget(surface, false); + if (!restoreTarget && m_prePopupFocusSurface + && m_prePopupFocusSurface->hasFocusCapability()) { + restoreTarget = m_prePopupFocusSurface; + } + } + + m_popupFocusStack.removeIf([surface](const QPointer &popup) { + return !popup || popup == surface; + }); + if (m_keyboardFocusSurface == surface) { - setKeyboardFocusSurface(nullptr); + setKeyboardFocusSurface(restoreTarget, Qt::ActiveWindowFocusReason); + qCInfo(lcTlPopupFocus) << "Focused surface destroyed" + << "seat" << m_seat->name() + << "surface" << surface + << "trackedPopup" << trackedPopup + << "restoredFocus" << restoreTarget; } + + if (m_popupFocusStack.isEmpty() && !m_popupKeyboardGrab) + m_prePopupFocusSurface.clear(); } void SeatSurfaceManager::givePopupFocus(SurfaceWrapper *popupWrapper) { - if (!m_hasPopupGrab) - return; - Q_ASSERT(popupWrapper); auto *popupSurface = qobject_cast(popupWrapper->shellSurface()); if (!popupSurface) @@ -444,69 +487,166 @@ void SeatSurfaceManager::givePopupFocus(SurfaceWrapper *popupWrapper) if (!wlrPopup || wlrPopup->seat != m_seat->handle()) return; + auto *seatHandle = m_seat->handle(); + auto *grab = seatHandle->keyboard_state.grab; + auto *popupGrab = keyboardGrabForPopup(wlrPopup); + if (!popupGrab || grab != popupGrab) { + qCWarning(lcTlPopupFocus) << "Refusing popup focus without its exact keyboard grab" + << "seat" << m_seat->name() + << "popup" << popupWrapper + << "expectedGrab" << popupGrab + << "currentGrab" << grab; + return; + } + + if (m_popupKeyboardGrab && m_popupKeyboardGrab != popupGrab) { + qCWarning(lcTlPopupFocus) << "Refusing popup focus while another structural grab is active" + << "seat" << m_seat->name() + << "popup" << popupWrapper + << "trackedGrab" << m_popupKeyboardGrab + << "popupGrab" << popupGrab; + return; + } + + if (!m_popupKeyboardGrab) { + m_popupKeyboardGrab = popupGrab; + m_prePopupFocusSurface = m_keyboardFocusSurface; + ++m_popupTransitionSerial; + qCInfo(lcTlPopupFocus) << "Popup keyboard grab tracked" + << "transition" << m_popupTransitionSerial + << "seat" << m_seat->name() + << "grab" << popupGrab + << "previousFocus" << m_prePopupFocusSurface; + } + + m_popupFocusStack.removeIf([](const QPointer &popup) { + return popup.isNull(); + }); + m_popupFocusStack.removeAll(popupWrapper); + m_popupFocusStack.append(popupWrapper); + // Move keyboard focus to the popup surface directly. setKeyboardFocusSurface(popupWrapper, Qt::ActiveWindowFocusReason); - qCDebug(lcTlPopupFocus) << "Moved keyboard focus to popup surface:" << popupWrapper; + qCInfo(lcTlPopupFocus) << "Popup received keyboard focus" + << "transition" << m_popupTransitionSerial + << "seat" << m_seat->name() + << "popup" << popupWrapper + << "parent" << popupWrapper->parentSurface() + << "depth" << m_popupFocusStack.size(); } void SeatSurfaceManager::dismissPopups() { - if (!m_hasPopupGrab) + m_popupFocusStack.removeIf([](const QPointer &popup) { + return popup.isNull(); + }); + if (!m_popupKeyboardGrab || m_popupFocusStack.isEmpty()) return; - qCDebug(lcTlPopupFocus) << "Dismissing popup grab"; - wlr_seat_keyboard_end_grab(m_seat->handle()); -} - -void SeatSurfaceManager::onKeyboardGrabBegin() -{ - if (m_hasPopupGrab) { - // Already tracking a popup grab; nested popups share the same flag. + auto *wrapper = m_popupFocusStack.constLast().data(); + auto *popup = wrapper + ? qobject_cast(wrapper->shellSurface()) + : nullptr; + if (!popup || !popup->handle()) { + qCWarning(lcTlPopupFocus) << "Unable to dismiss tracked popup" + << "seat" << m_seat->name() + << "wrapper" << wrapper + << "grab" << m_popupKeyboardGrab; return; } - auto *seatNative = m_seat->handle(); - auto *grab = seatNative->keyboard_state.grab; - if (!grab) { - qCWarning(lcTlPopupFocus) << "keyboard_state.grab is null"; - return; - } + qCInfo(lcTlPopupFocus) << "Dismissing concrete popup" + << "transition" << m_popupTransitionSerial + << "seat" << m_seat->name() + << "popup" << wrapper + << "grab" << m_popupKeyboardGrab; + popup->close(); +} - // Detect IME keyboard grab: - // WInputMethodHelper::handleNewKGV2 sets activeKeyboardGrab before - // calling keyboard_start_grab, so it is already non-null when we get here. - // Use isActiveKeyboardGrabOwner() to check if the seat's current grab - // is the one installed by the IME helper. - if (auto *imHelper = Helper::instance()->shellHandler()->inputMethodHelper()) { - if (imHelper->isActiveKeyboardGrabOwner()) { - qCDebug(lcTlPopupFocus) << "IME keyboard grab started (not popup)"; - return; - } +SurfaceWrapper *SeatSurfaceManager::popupParentFocusTarget(SurfaceWrapper *popup, + bool skipPopupParents) const +{ + auto *target = popup ? popup->parentSurface() : nullptr; + while (target) { + const bool isPopup = target->type() == SurfaceWrapper::Type::XdgPopup; + if ((!skipPopupParents || !isPopup) && target->hasFocusCapability()) + return target; + if (!isPopup) + break; + target = target->parentSurface(); } + return nullptr; +} - m_hasPopupGrab = true; - qCDebug(lcTlPopupFocus) << "Popup keyboard grab started"; +bool SeatSurfaceManager::isTrackedPopup(SurfaceWrapper *surface) const +{ + return std::any_of(m_popupFocusStack.cbegin(), m_popupFocusStack.cend(), + [surface](const QPointer &popup) { + return popup == surface; + }); } -void SeatSurfaceManager::onKeyboardGrabEnd() +void SeatSurfaceManager::onKeyboardGrabBegin(wlr_seat_keyboard_grab *grab) { - if (!m_hasPopupGrab) + if (!m_popupKeyboardGrab || grab == m_popupKeyboardGrab) return; - m_hasPopupGrab = false; - - qCDebug(lcTlPopupFocus) << "Popup keyboard grab ended, restoring focus to:" - << m_activatedSurface; + auto *oldGrab = m_popupKeyboardGrab; + m_popupKeyboardGrab = nullptr; + m_popupFocusStack.clear(); + m_prePopupFocusSurface.clear(); + ++m_popupTransitionSerial; + + // wlroots replaces keyboard_state.grab directly and does not emit an end + // event for the displaced grab. Forget the old popup state immediately; + // the new grab owns focus policy from this point onward. + qCInfo(lcTlPopupFocus) << "Popup keyboard grab replaced" + << "transition" << m_popupTransitionSerial + << "seat" << m_seat->name() + << "oldGrab" << oldGrab + << "newGrab" << grab + << "currentFocus" << m_keyboardFocusSurface; +} - // While showing the desktop, keyboard focus is on the desktop layer, not on the - // (hidden) activated surface; do not yank it back to the window. - if (auto *helper = Helper::instance()) { - if (helper->showDesktopState() == ShowDesktopInterfaceV1::State::Show) - return; +void SeatSurfaceManager::onKeyboardGrabEnd(wlr_seat_keyboard_grab *grab) +{ + if (!m_popupKeyboardGrab || grab != m_popupKeyboardGrab) { + qCDebug(lcTlPopupFocus) << "Ignoring unrelated keyboard grab end" + << "seat" << m_seat->name() + << "endedGrab" << grab + << "popupGrab" << m_popupKeyboardGrab; + return; } - if (m_activatedSurface && m_activatedSurface->hasFocusCapability()) { - setKeyboardFocusSurface(m_activatedSurface, Qt::ActiveWindowFocusReason); + QPointer currentFocus = m_keyboardFocusSurface; + QPointer restoreTarget; + const bool popupStillFocused = currentFocus && isTrackedPopup(currentFocus); + if (popupStillFocused) + restoreTarget = popupParentFocusTarget(currentFocus, true); + if (!restoreTarget && m_prePopupFocusSurface + && m_prePopupFocusSurface->hasFocusCapability()) { + restoreTarget = m_prePopupFocusSurface; } + + m_popupKeyboardGrab = nullptr; + m_popupFocusStack.clear(); + m_prePopupFocusSurface.clear(); + ++m_popupTransitionSerial; + + // While showing the desktop, keyboard focus is on the desktop layer, not on + // the (hidden) activated surface; do not yank it back to the window. + const bool showingDesktop = Helper::instance() + && Helper::instance()->showDesktopState() == ShowDesktopInterfaceV1::State::Show; + + if (popupStillFocused && !showingDesktop) + setKeyboardFocusSurface(restoreTarget, Qt::ActiveWindowFocusReason); + + qCInfo(lcTlPopupFocus) << "Popup keyboard grab ended" + << "transition" << m_popupTransitionSerial + << "seat" << m_seat->name() + << "endedGrab" << grab + << "previousFocus" << currentFocus + << "restoredFocus" << restoreTarget + << "activationUnchanged" << m_activatedSurface; } diff --git a/src/surface/seatsurfacemanager.h b/src/surface/seatsurfacemanager.h index 9c3bb3503e..5eb49246df 100644 --- a/src/surface/seatsurfacemanager.h +++ b/src/surface/seatsurfacemanager.h @@ -65,7 +65,7 @@ class SeatSurfaceManager : public QObject // Popup keyboard grab management void givePopupFocus(SurfaceWrapper *popupWrapper); void dismissPopups(); - bool hasPopupGrab() const { return m_hasPopupGrab; } + bool hasPopupGrab() const { return m_popupKeyboardGrab != nullptr; } Q_SIGNALS: void activatedSurfaceChanged(SurfaceWrapper *surface); @@ -73,8 +73,10 @@ class SeatSurfaceManager : public QObject private: void onActivatedSurfaceFocusCapabilityChanged(); - void onKeyboardGrabBegin(); - void onKeyboardGrabEnd(); + void onKeyboardGrabBegin(wlr_seat_keyboard_grab *grab); + void onKeyboardGrabEnd(wlr_seat_keyboard_grab *grab); + SurfaceWrapper *popupParentFocusTarget(SurfaceWrapper *popup, bool skipPopupParents) const; + bool isTrackedPopup(SurfaceWrapper *surface) const; WSeat *m_seat = nullptr; RootSurfaceContainer *m_rootContainer = nullptr; @@ -86,7 +88,10 @@ class SeatSurfaceManager : public QObject bool m_metaKeyPressed = false; // Popup grab state - bool m_hasPopupGrab = false; + wlr_seat_keyboard_grab *m_popupKeyboardGrab = nullptr; + QPointer m_prePopupFocusSurface; + QList> m_popupFocusStack; + quint64 m_popupTransitionSerial = 0; QTimer *m_edgeTileDelayTimer = nullptr; bool m_resizeClampActive = false; diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 7226b3daa9..df951025d5 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -2281,6 +2281,14 @@ bool SurfaceWrapper::stackAfter(QQuickItem *item) break; } item = s->stackLastSurface(); + // The deepest surface of s's sub-chain may live in a different + // QQuickItem container than ours (X11 transient children can be + // placed by separate layering rules). QQuickItem::stackAfter + // silently refuses non-siblings and would then abort the whole + // raise with the stacking bookkeeping left out of sync; fall back + // to s itself, which the entry guard above validated. + if (!item || item->parentItem() != parentItem()) + item = s; if (m_parentSurface && m_parentSurface == s->m_parentSurface) { QQuickItem::stackAfter(item); diff --git a/src/systemd-socket.cpp b/src/systemd-socket.cpp index bcc89d84d4..6a764e65eb 100644 --- a/src/systemd-socket.cpp +++ b/src/systemd-socket.cpp @@ -7,8 +7,12 @@ #include #include #include +#include #include +#include #include +#include +#include #include #include #include @@ -24,6 +28,9 @@ #include #include +#include +#include +#include #include #include #include @@ -64,6 +71,10 @@ class SocketActivator : public QObject if (tryStart(QDBusConnection::sessionBus()) || tryStart(QDBusConnection::systemBus())) { m_started = true; clearPendingRetry(StartRetry); + // Fresh compositor (re-)registration: start a new activation window. + m_waylandActivateFailures = 0; + m_handoverAttempts = 0; + clearPendingRetry(HandoverRetry); activate(); return; } @@ -87,11 +98,34 @@ public Q_SLOTS: if (updateFd.isValid()) { if (m_type == "wayland") { - if (!callDBus(updateFd, + const auto reply = callDBus(updateFd, QStringLiteral("ActivateWayland"), QStringLiteral("Failed to activate Wayland socket"), - QVariant::fromValue(*m_unixFileDescriptor))) { - return; + QVariant::fromValue(*m_unixFileDescriptor)); + // ReplyMessage carrying `false` means the compositor is on the bus + // but not ready to accept this session's socket (e.g. its user + // session is not registered yet during the login handover). Exporting + // the environment then would start autostart services (fcitx5!) + // against a display that nobody is serving, and they never retry. + // Stay unnotified: Ready=1 (and with it ExecStartPost and + // dde-session-pre.target) must gate the whole session on a socket + // that was really activated. + const bool socketAccepted = reply && reply->arguments().value(0).toBool(); + if (!socketAccepted) { + ++m_waylandActivateFailures; + if (m_waylandActivateFailures <= MaxWaylandActivateRetries) { + qCWarning(lcSdSocket) << "Wayland socket activation not accepted yet, retrying" + << m_waylandActivateFailures; + scheduleActivateRetry(); + return; + } + // Degraded path (compositor persistently refuses): still export + // the environment and notify readiness, matching the historic + // behaviour so the session can never hang on a broken + // activation, while the retry window above covers the normal + // login-handover race. + qCWarning(lcSdSocket) + << "Wayland socket activation repeatedly refused, publishing environment anyway"; } QDBusInterface dbus("org.freedesktop.DBus", @@ -99,7 +133,7 @@ public Q_SLOTS: "org.freedesktop.DBus", QDBusConnection::sessionBus()); StringMap env; - env["WAYLAND_DISPLAY"] = "treeland.socket"; + env["WAYLAND_DISPLAY"] = waylandDisplayName(); const auto extraEnvs = qgetenv("TREELAND_SESSION_ENVIRONMENTS"); if (!extraEnvs.isEmpty()) { @@ -120,6 +154,12 @@ public Q_SLOTS: } sd_notify(0, "READY=1"); + + // Only a socket the compositor really accepted can serve the + // input method. In the degraded path above nothing is listening + // on it, so the running fcitx5 must keep whatever it has. + if (socketAccepted) + handWaylandSocketToInputMethod(); } else if (m_type == "xwayland") { QDBusMessage reply = updateFd.call("XWaylandName"); if (reply.type() == QDBusMessage::ReplyMessage) { @@ -159,6 +199,34 @@ public Q_SLOTS: return; } + // Same half of the publication as dde-session's + // EnvironmentsManager does (systemd1.SetEnvironment in + // addition to UpdateActivationEnvironment), mirroring the + // WAYLAND_DISPLAY/QT_IM_MODULE/*_IM_MODULE set-environment + // the wayland unit posts via ExecStartPost: transient + // session units (and hence services like fcitx5 started + // via StartTransientUnit) inherit the *manager* + // environment, not the activation one. Without this, + // X11-side input methods (the fcitx5 X selection / XIM on + // the XWayland display) never see DISPLAY/XAUTHORITY in a + // treeland session, while KWin-based sessions only work + // because dde-session performs exactly this call. + { + QDBusInterface systemd1("org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + QDBusConnection::sessionBus()); + if (systemd1.isValid()) { + QStringList envList; + envList << QStringLiteral("DISPLAY=%1").arg(xwaylandName) + << QStringLiteral("XAUTHORITY=%1").arg(authFileName); + callDBus(systemd1, + QStringLiteral("SetEnvironment"), + QStringLiteral("Failed to set XWayland session environment"), + envList); + } + } + sd_notify(0, "READY=1"); m_lastXwaylandAuth = auth; clearPendingRetry(ActivateRetry); @@ -181,6 +249,7 @@ public Q_SLOTS: enum RetryFlag { StartRetry = 1 << 0, ActivateRetry = 1 << 1, + HandoverRetry = 1 << 2, }; bool isRetryPending(RetryFlag flag) const { @@ -219,6 +288,52 @@ public Q_SLOTS: return runtimeFileName(QStringLiteral("treeland-xauthority")); } + // Name of the Wayland socket this session is built on. Must stay in sync + // with the ListenStream of treeland-sd.socket, the WAYLAND_DISPLAY + // published below and the one ExecStartPost sets in the user manager. + static QString waylandDisplayName() + { + return QStringLiteral("treeland.socket"); + } + + // Connect to a listening AF_UNIX socket, returning an owned fd or -1. + int connectToSocket(const QString &path) const + { + const QByteArray encodedPath = QFile::encodeName(path); + + sockaddr_un address{}; + address.sun_family = AF_UNIX; + if (encodedPath.size() + 1 > static_cast(sizeof(address.sun_path))) { + errno = ENAMETOOLONG; + return -1; + } + memcpy(address.sun_path, encodedPath.constData(), static_cast(encodedPath.size())); + + int fd = -1; + do { + fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + } while (fd < 0 && errno == EINTR); + if (fd < 0) + return -1; + + int result = -1; + do { + result = ::connect( + fd, + reinterpret_cast(&address), + static_cast(offsetof(sockaddr_un, sun_path) + encodedPath.size() + 1)); + } while (result < 0 && errno == EINTR); + + if (result < 0) { + const int error = errno; + ::close(fd); + errno = error; + return -1; + } + + return fd; + } + bool writeXAuthority(const QString &fileName, const QByteArray &auth) const { QSaveFile file(fileName); if (!file.open(QFile::WriteOnly)) { @@ -332,7 +447,7 @@ public Q_SLOTS: } void scheduleActivateRetry() { - if (m_type != "xwayland" || isRetryPending(ActivateRetry)) + if (isRetryPending(ActivateRetry)) return; setPendingRetry(ActivateRetry); @@ -353,12 +468,108 @@ public Q_SLOTS: clearPendingRetry(ActivateRetry); } + // fcitx5 connects to Wayland exactly once at startup and never retries, so + // a compositor restart leaves it alive without its Wayland input-method + // frontend until it is restarted by hand. It does offer a D-Bus call to + // replace that connection, which is safe to use right after the socket was + // activated. Reopen (instead of Open) keeps fcitx5 on a single main + // connection, so the compositor never sees a second input method. + void handWaylandSocketToInputMethod() + { + if (m_type != "wayland" || m_handoverAttempts >= MaxHandoverAttempts) + return; + + // Never let D-Bus activation start fcitx5 just because it is not + // running: without it there is nothing to reconnect. + const auto *busInterface = QDBusConnection::sessionBus().interface(); + if (!busInterface + || !busInterface->isServiceRegistered(QStringLiteral("org.fcitx.Fcitx5")).value()) { + qCDebug(lcSdSocket) + << "Input method is not running, nothing to hand the Wayland socket to"; + return; + } + + ++m_handoverAttempts; + + const QString displayName = waylandDisplayName(); + const int fd = connectToSocket(runtimeFileName(displayName)); + if (fd < 0) { + const int error = errno; + qCWarning(lcSdSocket) << "Failed to connect to the Wayland socket" << displayName + << "for the input method:" << strerror(error); + scheduleHandoverRetry(); + return; + } + + QDBusUnixFileDescriptor unixFileDescriptor(fd); + // The message owns a duplicate from here on. + ::close(fd); + if (!unixFileDescriptor.isValid()) { + qCWarning(lcSdSocket) << "Failed to duplicate the file descriptor of" << displayName; + scheduleHandoverRetry(); + return; + } + + QDBusMessage message = + QDBusMessage::createMethodCall(QStringLiteral("org.fcitx.Fcitx5"), + QStringLiteral("/controller"), + QStringLiteral("org.fcitx.Fcitx.Controller1"), + QStringLiteral("ReopenWaylandConnectionSocket")); + message.setAutoStartService(false); + message << displayName << QVariant::fromValue(unixFileDescriptor); + + auto *watcher = + new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message), this); + connect(watcher, + &QDBusPendingCallWatcher::finished, + this, + &SocketActivator::handoverFinished); + } + + void handoverFinished(QDBusPendingCallWatcher *watcher) + { + watcher->deleteLater(); + if (watcher->isError()) { + qCWarning(lcSdSocket) << "Input method rejected the Wayland socket:" + << watcher->error().message(); + scheduleHandoverRetry(); + return; + } + + qCInfo(lcSdSocket) << "Handed a fresh Wayland socket to the input method"; + } + + void scheduleHandoverRetry() + { + if (isRetryPending(HandoverRetry) || m_handoverAttempts >= MaxHandoverAttempts) + return; + + setPendingRetry(HandoverRetry); + QTimer::singleShot(HandoverRetryIntervalMs, this, [this] { + if (!isRetryPending(HandoverRetry)) + return; + + clearPendingRetry(HandoverRetry); + handWaylandSocketToInputMethod(); + }); + } + static constexpr int RetryIntervalMs = 500; + // fcitx5 may still be initializing when its D-Bus name shows up, so allow + // one delayed retry before giving up until the next activation. + static constexpr int HandoverRetryIntervalMs = RetryIntervalMs * 2; + static constexpr int MaxHandoverAttempts = 2; + // Upper bound for the login-handover retry window (40 * 500ms), after which + // we degrade to the historic publish-anyway behaviour rather than hanging + // the session forever on a broken compositor. + static constexpr int MaxWaylandActivateRetries = 40; std::shared_ptr m_unixFileDescriptor; QString m_type; bool m_started = false; int m_pendingRetries = 0; + int m_waylandActivateFailures = 0; + int m_handoverAttempts = 0; QByteArray m_lastXwaylandAuth; std::optional m_compositorBus; }; diff --git a/waylib/src/server/kernel/wseat.cpp b/waylib/src/server/kernel/wseat.cpp index 48b90d7d9c..34052bca34 100644 --- a/waylib/src/server/kernel/wseat.cpp +++ b/waylib/src/server/kernel/wseat.cpp @@ -71,12 +71,20 @@ class Q_DECL_HIDDEN WSeatPrivate : public WObjectPrivate pendingEvents.reserve(2); m_repeatTimer.callOnTimeout([&](){ - if (!focusWindow) { + if (!focusWindow || !m_repeatKey) { + m_repeatTimer.stop(); + m_repeatKey.reset(); return; } - auto rawdevice = WInputDevice::from(m_repeatKey->device())->handle(); - auto wlrKeyboard = rawdevice->type == WLR_INPUT_DEVICE_KEYBOARD + auto inputDevice = WInputDevice::from(m_repeatKey->device()); + auto rawdevice = inputDevice ? inputDevice->handle() : nullptr; + auto wlrKeyboard = rawdevice && rawdevice->type == WLR_INPUT_DEVICE_KEYBOARD ? wlr_keyboard_from_input_device(rawdevice) : nullptr; + if (!wlrKeyboard || wlrKeyboard->repeat_info.rate <= 0) { + m_repeatTimer.stop(); + m_repeatKey.reset(); + return; + } m_repeatTimer.setInterval(1000 / wlrKeyboard->repeat_info.rate); auto evPress = QKeyEvent(QEvent::KeyPress, m_repeatKey->key(), m_repeatKey->modifiers(), m_repeatKey->nativeScanCode(), m_repeatKey->nativeVirtualKey(), m_repeatKey->nativeModifiers(), @@ -223,7 +231,11 @@ class Q_DECL_HIDDEN WSeatPrivate : public WObjectPrivate const wlr_keyboard_modifiers *modifiers = nullptr; const uint32_t *keycodes = nullptr; size_t numKeycodes = 0; - auto keyboard = q_func()->keyboard(); + // Prefer the keyboard group for the enter payload: while a text + // input is active the seat's current keyboard may be the input + // method's virtual keyboard, whose (empty or stale) pressed-key + // and modifier state must never leak into focus transitions. + auto keyboard = groupkeyboardDevice ? groupkeyboardDevice : q_func()->keyboard(); if (keyboard) { auto *wlr_keyboard = wlr_keyboard_from_input_device(keyboard->handle()); if (wlr_keyboard) { @@ -313,8 +325,12 @@ class Q_DECL_HIDDEN WSeatPrivate : public WObjectPrivate // for keyboard event inline bool doNotifyKey(WInputDevice *device, uint32_t keycode, uint32_t state, uint32_t timestamp) { - q_func()->setKeyboard(device); + if (keyboardFocusSurface() && keyboardFilter + && keyboardFilter->filterKey(q_func(), device, keycode, state, timestamp)) { + return true; + } + q_func()->setKeyboard(device); if (!keyboardFocusSurface()) return false; @@ -325,6 +341,11 @@ class Q_DECL_HIDDEN WSeatPrivate : public WObjectPrivate inline bool doNotifyModifiers(WInputDevice *device) { auto keyboard = wlr_keyboard_from_input_device(device->handle()); + if (keyboardFocusSurface() && keyboardFilter + && keyboardFilter->filterModifiers(q_func(), device, &keyboard->modifiers)) { + return true; + } + // wlr_seat_set_keyboard() already sends modifiers when the keyboard // changes, so skip the explicit send to avoid a duplicate. bool keyboardChanged = (q_func()->keyboard() != device); @@ -383,6 +404,7 @@ class Q_DECL_HIDDEN WSeatPrivate : public WObjectPrivate QVector deviceList; QVector touchDeviceList; QPointer eventFilter; + WSeatKeyboardFilter *keyboardFilter = nullptr; QPointer focusWindow; QPointer pointerFocusEventObject; QPointer m_keyboardFocusSurface; @@ -579,6 +601,14 @@ void WSeatPrivate::on_keyboard_key(wlr_keyboard_key_event *event, WInputDevice * { auto keyboard = wlr_keyboard_from_input_device(device->handle()); + if (focusWindow && keyboardFocusSurface() && keyboardFilter + && keyboardFilter->filterKey(q_func(), device, event->keycode, event->state, + event->time_msec)) { + m_repeatTimer.stop(); + m_repeatKey.reset(); + return; + } + auto code = event->keycode + 8; // map to wl_keyboard::keymap_format::keymap_format_xkb_v1 auto et = event->state == WL_KEYBOARD_KEY_STATE_PRESSED ? QEvent::KeyPress : QEvent::KeyRelease; xkb_keysym_t sym = xkb_state_key_get_one_sym(keyboard->xkb_state, code); @@ -609,7 +639,8 @@ void WSeatPrivate::on_keyboard_key(wlr_keyboard_key_event *event, WInputDevice * if (focusWindow) { handleKeyEvent(e); - if (et == QEvent::KeyPress && xkb_keymap_key_repeats(keyboard->keymap, code)) { + if (et == QEvent::KeyPress && keyboard->repeat_info.rate > 0 + && xkb_keymap_key_repeats(keyboard->keymap, code)) { if (m_repeatKey) { m_repeatTimer.stop(); } @@ -1167,6 +1198,8 @@ void WSeat::setKeyboardFocusSurface(WSurface *surface) setKeyboard(d->groupkeyboardDevice); } + d->m_repeatTimer.stop(); + d->m_repeatKey.reset(); d->m_keyboardFocusSurface = surface; if (isValid()) d->doSetKeyboardFocus(surface ? surface->handle() : nullptr); @@ -1183,12 +1216,22 @@ WSurface *WSeat::keyboardFocusSurface() const void WSeat::clearKeyboardFocusSurface() { W_D(WSeat); + d->m_repeatTimer.stop(); + d->m_repeatKey.reset(); d->doSetKeyboardFocus(nullptr); + // Keep the wrapper view and the focus-change signal consistent with the + // raw seat state; listeners (input-method helper) reconcile on it. + d->m_keyboardFocusSurface = nullptr; + Q_EMIT keyboardFocusSurfaceChanged(); } void WSeat::setKeyboardFocusWindow(QWindow *window) { W_D(WSeat); + if (d->focusWindow == window) + return; + d->m_repeatTimer.stop(); + d->m_repeatKey.reset(); d->focusWindow = window; } @@ -1201,6 +1244,8 @@ QWindow *WSeat::keyboardFocusWindow() const void WSeat::clearKeyboardFocusWindow() { W_D(WSeat); + d->m_repeatTimer.stop(); + d->m_repeatKey.reset(); d->focusWindow = nullptr; } @@ -1630,6 +1675,19 @@ void WSeat::setEventFilter(WSeatEventFilter *filter) d->eventFilter = filter; } +WSeatKeyboardFilter *WSeat::keyboardFilter() const +{ + W_DC(WSeat); + return d->keyboardFilter; +} + +void WSeat::setKeyboardFilter(WSeatKeyboardFilter *filter) +{ + W_D(WSeat); + Q_ASSERT(!filter || !d->keyboardFilter || d->keyboardFilter == filter); + d->keyboardFilter = filter; +} + void WSeat::create(WServer *server) { W_D(WSeat); @@ -1678,6 +1736,7 @@ void WSeat::destroy(WServer *) i->setSeat(nullptr); d->deviceList.clear(); + d->keyboardFilter = nullptr; // Need not call the DCursor::detachInputDevice on destroy WSeat, so do // call the detachCursor at clear the deviceList after. diff --git a/waylib/src/server/kernel/wseat.h b/waylib/src/server/kernel/wseat.h index 5cf5308fb2..66d422d360 100644 --- a/waylib/src/server/kernel/wseat.h +++ b/waylib/src/server/kernel/wseat.h @@ -28,6 +28,17 @@ WAYLIB_SERVER_BEGIN_NAMESPACE class WSurface; class WSeat; +class WAYLIB_SERVER_EXPORT WSeatKeyboardFilter +{ +public: + virtual ~WSeatKeyboardFilter() = default; + + virtual bool filterKey(WSeat *seat, WInputDevice *device, uint32_t keycode, + uint32_t state, uint32_t timestamp) = 0; + virtual bool filterModifiers(WSeat *seat, WInputDevice *device, + const wlr_keyboard_modifiers *modifiers) = 0; +}; + class WAYLIB_SERVER_EXPORT WSeatEventFilter : public QObject { friend class WSeat; @@ -86,6 +97,8 @@ class WAYLIB_SERVER_EXPORT WSeat : public QObject, public WObject, public WServe WSeatEventFilter *eventFilter() const; void setEventFilter(WSeatEventFilter *filter); + WSeatKeyboardFilter *keyboardFilter() const; + void setKeyboardFilter(WSeatKeyboardFilter *filter); WSurface *pointerFocusSurface() const; diff --git a/waylib/src/server/protocols/private/wtextinputv1.cpp b/waylib/src/server/protocols/private/wtextinputv1.cpp index 2354fd9cac..cefeddb366 100644 --- a/waylib/src/server/protocols/private/wtextinputv1.cpp +++ b/waylib/src/server/protocols/private/wtextinputv1.cpp @@ -11,6 +11,7 @@ #include #include +#include #include extern "C" { @@ -33,7 +34,14 @@ class Q_DECL_HIDDEN WTextInputV1Private : public WTextInputPrivate wl_resource *resource {nullptr}; WSeat *seat {nullptr}; - WSurface *focusedSurface {nullptr}; + // Client-owned activation record: set by the activate request and only + // cleared by a client deactivation or the surface's destruction. Server + // driven enter/leave notifications do not erase it, so keyboard focus + // returning to the activated surface can re-arm the text input. + QPointer focusedSurface; + // True after an enter event has been sent to the client and no matching + // leave has been sent yet; keeps enter/leave paired. + bool entered {false}; uint32_t currentSerial {0}; bool active {false}; QString surroundingText {}; @@ -139,20 +147,29 @@ IME::Features WTextInputV1::features() const void WTextInputV1::sendEnter(WSurface *surface) { + W_D(WTextInputV1); // Note: For text input v1, activation and surface focus is managed by client. // Do not send focus to text input unless it's activated. - if (d_func()->active) - zwp_text_input_v1_send_enter(d_func()->resource, surface->handle()->resource); + if (d->active && !d->entered) { + zwp_text_input_v1_send_enter(d->resource, surface->handle()->resource); + d->entered = true; + } Q_EMIT this->enabled(); } void WTextInputV1::sendLeave() { - if (focusedSurface()) { - W_D(WTextInputV1); - d->focusedSurface = nullptr; - zwp_text_input_v1_send_leave(d_func()->resource); - Q_EMIT disabled(); + W_D(WTextInputV1); + if (!d->focusedSurface) { + return; + } + // A server-side leave is only a notification: it neither clears the + // client-owned activation record nor revokes the enablement. Only a + // client-side deactivate or destruction of the activated surface does + // (see text_input_handle_deactivate and the beforeDestroy connection). + if (d->entered) { + zwp_text_input_v1_send_leave(d->resource); + d->entered = false; } } @@ -242,8 +259,15 @@ void text_input_handle_activate([[maybe_unused]] wl_client *client, if (text_input->focusedSurface()) text_input->focusedSurface()->disconnect(text_input); d->focusedSurface = wSurface; - QObject::connect(wSurface, &WSurface::beforeDestroy, - text_input, &WTextInputV1::sendLeave); + d->entered = false; + // Destroying the activated surface ends its activation for real (this + // path alone, unlike a plain server-side leave, revokes the record). + QObject::connect(wSurface, &WSurface::beforeDestroy, text_input, [text_input] { + auto *dd = text_input->d_func(); + dd->focusedSurface = nullptr; + dd->entered = false; + Q_EMIT text_input->disabled(); + }); } d->active = true; Q_EMIT text_input->activate(); @@ -262,7 +286,14 @@ void text_input_handle_deactivate([[maybe_unused]] wl_client *client, d->seat = nullptr; d->active = false; + // The client revokes its own activation: this is an authoritative + // disablement (a server-side leave alone no longer is one). Emit while + // the record still exists so the chained requestLeave/sendLeave below can + // pair the native leave event. + Q_EMIT text_input->disabled(); Q_EMIT text_input->deactivate(); + d->focusedSurface = nullptr; + d->entered = false; } void text_input_handle_show_input_panel([[maybe_unused]] wl_client *client, diff --git a/waylib/src/server/protocols/private/wtextinputv2.cpp b/waylib/src/server/protocols/private/wtextinputv2.cpp index fbb86f8e76..7af99369d7 100644 --- a/waylib/src/server/protocols/private/wtextinputv2.cpp +++ b/waylib/src/server/protocols/private/wtextinputv2.cpp @@ -52,6 +52,7 @@ class Q_DECL_HIDDEN WTextInputV2Private : public WTextInputPrivate WClient *client; WSurface *enabledSurface; WSurface *focusedSurface; + QMetaObject::Connection enabledSurfaceDestroyConnection; // Tracks the beforeDestroy connection of the focused surface. Qt 6 does // not support Qt::UniqueConnection with a functor target (it asserts in // debug builds), so the connection is managed explicitly. @@ -211,8 +212,9 @@ void handle_text_input_enable([[maybe_unused]] wl_client *client, wl_resource *r text_input->clearEnabledSurface(); } d->enabledSurface = wSurface; - QObject::connect(wSurface, &WSurface::beforeDestroy, - text_input, &WTextInputV2::clearEnabledSurface); + d->enabledSurfaceDestroyConnection = QObject::connect( + wSurface, &WSurface::beforeDestroy, + text_input, &WTextInputV2::clearEnabledSurface); Q_EMIT text_input->enableOnSurface(wSurface); } @@ -393,6 +395,13 @@ IME::Features WTextInputV2::features() const void WTextInputV2::sendEnter(WSurface *surface) { W_D(WTextInputV2); + Q_ASSERT(surface); + if (!surface || d->focusedSurface == surface) + return; + + if (d->focusedSurface) + sendLeave(); + d->focusedSurface = surface; // Qt::UniqueConnection is only supported for member function targets and // asserts with a functor, so replace the previous focus-destroy @@ -403,10 +412,14 @@ void WTextInputV2::sendEnter(WSurface *surface) } d->focusedSurfaceDestroyConnection = QObject::connect(surface, &WSurface::beforeDestroy, this, [this, d, surface] { - if (d->focusedSurface == surface) - sendLeave(); - }); + if (d->focusedSurface == surface) + sendLeave(); + }); zwp_text_input_v2_send_enter(d->resource, 0, surface->handle()->resource); + qCDebug(lcWlTextInput) << "Text input v2 focus entered" + << "textInput" << this + << "surface" << surface + << "enabledSurface" << d->enabledSurface; if (d->enabledSurface == d->focusedSurface) { Q_EMIT enabled(); } @@ -416,19 +429,29 @@ void WTextInputV2::sendLeave() { W_D(WTextInputV2); if (!d->focusedSurface) { - qCWarning(lcWlTextInput()) << "Send leave to a null focused surface."; + qCDebug(lcWlTextInput) << "Ignoring duplicate text input v2 leave" + << "textInput" << this; return; } - zwp_text_input_v2_send_leave(d->resource, 0, d->focusedSurface->handle()->resource); - if (d->enabledSurface == d->focusedSurface) { - Q_EMIT disabled(); - } - d->focusedSurface = nullptr; + + auto *oldSurface = d->focusedSurface; + const bool wasEnabled = d->enabledSurface == oldSurface; // The focused surface no longer needs its destroy notification. if (d->focusedSurfaceDestroyConnection) { QObject::disconnect(d->focusedSurfaceDestroyConnection); d->focusedSurfaceDestroyConnection = {}; } + d->focusedSurface = nullptr; + zwp_text_input_v2_send_leave(d->resource, 0, oldSurface->handle()->resource); + qCDebug(lcWlTextInput) << "Text input v2 focus left" + << "textInput" << this + << "surface" << oldSurface + << "wasEnabled" << wasEnabled; + // Note: a compositor-driven leave is only a notification. It must NOT + // emit disabled(): the client's own disable request (disableOnSurface) + // or the enabled surface's destruction is what revokes the enablement + // the input-method helper anchors on. Eligibility additionally requires + // focusedSurface() to match the seat focus, which is cleared right here. } void WTextInputV2::sendDone() @@ -454,15 +477,18 @@ void WTextInputV2::handleIMCommitted(WInputMethodV2 *im) WTextInputV2::WTextInputV2(QObject *parent) : WTextInput(*new WTextInputV2Private(this), parent) { - connect(this, &WTextInputV2::enableOnSurface, this, [this] { - if (focusedSurface()) { + connect(this, &WTextInputV2::enableOnSurface, this, [this](WSurface *surface) { + if (focusedSurface() == surface) { Q_EMIT enabled(); } }); - connect(this, &WTextInputV2::disableOnSurface, this, [this] { - if (!focusedSurface()) { - Q_EMIT disabled(); - } + connect(this, &WTextInputV2::disableOnSurface, this, [this](WSurface *) { + // clearEnabledSurface() runs only for an authoritative end of the + // client-side enablement (explicit disable request, or the enabled + // surface's destruction). Compositor-driven leave no longer emits + // disabled(), so revoke unconditionally here regardless of which + // surface currently holds keyboard focus. + Q_EMIT disabled(); }); connect(this, &WTextInput::enabled, this, [this]{ qCDebug(lcWlTextInput()) << "text input v2" << this << "enabled"; @@ -494,7 +520,10 @@ void WTextInputV2::clearEnabledSurface() W_D(WTextInputV2); Q_ASSERT(d->enabledSurface); Q_EMIT disableOnSurface(d->enabledSurface); - d->enabledSurface->disconnect(this); + if (d->enabledSurfaceDestroyConnection) { + QObject::disconnect(d->enabledSurfaceDestroyConnection); + d->enabledSurfaceDestroyConnection = {}; + } d->enabledSurface = nullptr; } WAYLIB_SERVER_END_NAMESPACE diff --git a/waylib/src/server/protocols/winputmethodhelper.cpp b/waylib/src/server/protocols/winputmethodhelper.cpp index dc9a813b3e..1dd9009ce6 100644 --- a/waylib/src/server/protocols/winputmethodhelper.cpp +++ b/waylib/src/server/protocols/winputmethodhelper.cpp @@ -17,60 +17,76 @@ #include -#include - #include +#include + WAYLIB_SERVER_BEGIN_NAMESPACE -struct Q_DECL_HIDDEN GrabHandlerArg { - const WInputMethodHelper *const helper; - wlr_input_method_keyboard_grab_v2 *grab; +namespace { + +struct KeyboardStateSnapshot +{ + xkb_mod_mask_t depressed = 0; + xkb_mod_mask_t latched = 0; + xkb_mod_mask_t locked = 0; + xkb_layout_index_t layout = 0; }; -void handleKey(struct wlr_seat_keyboard_grab *grab, uint32_t time_msec, uint32_t key, uint32_t state) +KeyboardStateSnapshot snapshotKeyboardState(struct xkb_state *state) { - auto arg = reinterpret_cast(grab->data); - if (!arg->grab) { - qCCritical(lcWlInputMethod) << "Ignore key event for destroyed input method keyboard grab" - << "key" << key << "state" << state; - return; - } - for (auto vk: arg->helper->virtualKeyboards()) { - if (wlr_keyboard_from_input_device(vk->handle()) == grab->seat->keyboard_state.keyboard) { - auto *virtualKeyboard = wlr_input_device_get_virtual_keyboard(vk->handle()); - if (virtualKeyboard && virtualKeyboard->resource && arg->grab->resource - && wl_resource_get_client(virtualKeyboard->resource) - == wl_resource_get_client(arg->grab->resource)) { - grab->seat->keyboard_state.default_grab->interface->key(grab, time_msec, key, state); - return; - } + KeyboardStateSnapshot snap; + snap.depressed = xkb_state_serialize_mods(state, XKB_STATE_MODS_DEPRESSED); + snap.latched = xkb_state_serialize_mods(state, XKB_STATE_MODS_LATCHED); + snap.locked = xkb_state_serialize_mods(state, XKB_STATE_MODS_LOCKED); + snap.layout = xkb_state_serialize_layout(state, XKB_STATE_LAYOUT_EFFECTIVE); + return snap; +} + +// Resolve a keysym to a (keycode, needsShift) pair from the given keymap and +// state. Uppercase/symbol keysyms are searched with Shift added to the +// currently effective modifier set. +bool findKeysymWithShift(struct xkb_keymap *keymap, struct xkb_state *state, + xkb_keysym_t keysym, xkb_keycode_t *keycodeOut, bool *shiftOut) +{ + const xkb_keycode_t first = xkb_keymap_min_keycode(keymap); + const xkb_keycode_t last = xkb_keymap_max_keycode(keymap); + for (xkb_keycode_t kc = first; kc <= last; ++kc) { + if (xkb_state_key_get_one_sym(state, kc) == keysym) { + *keycodeOut = kc; + *shiftOut = false; + return true; } } - wlr_input_method_keyboard_grab_v2_send_key(arg->grab, time_msec, key, state); -} -void handleModifiers(struct wlr_seat_keyboard_grab *grab, const struct wlr_keyboard_modifiers *modifiers) -{ - auto arg = reinterpret_cast(grab->data); - if (!arg->grab) { - qCCritical(lcWlInputMethod) << "Ignore modifiers for destroyed input method keyboard grab"; - return; + const xkb_mod_index_t shiftIndex = xkb_keymap_mod_get_index(keymap, XKB_MOD_NAME_SHIFT); + if (shiftIndex == XKB_MOD_INVALID) { + return false; } - for (auto vk: arg->helper->virtualKeyboards()) { - if (wlr_keyboard_from_input_device(vk->handle()) == grab->seat->keyboard_state.keyboard) { - auto *virtualKeyboard = wlr_input_device_get_virtual_keyboard(vk->handle()); - if (virtualKeyboard && virtualKeyboard->resource && arg->grab->resource - && wl_resource_get_client(virtualKeyboard->resource) - == wl_resource_get_client(arg->grab->resource)) { - grab->seat->keyboard_state.default_grab->interface->modifiers(grab, modifiers); - return; + const KeyboardStateSnapshot snap = snapshotKeyboardState(state); + struct xkb_state *shifted = xkb_state_new(keymap); + if (!shifted) { + return false; + } + bool found = false; + if (xkb_state_update_mask(shifted, snap.depressed | (xkb_mod_mask_t(1) << shiftIndex), + snap.latched, snap.locked, 0, 0, snap.layout) == 0) { + for (xkb_keycode_t kc = first; kc <= last; ++kc) { + if (xkb_state_key_get_one_sym(shifted, kc) == keysym) { + *keycodeOut = kc; + *shiftOut = true; + found = true; + break; } } } - wlr_input_method_keyboard_grab_v2_send_modifiers(arg->grab, const_cast(modifiers)); + xkb_state_unref(shifted); + return found; } -class Q_DECL_HIDDEN WInputMethodHelperPrivate : public WObjectPrivate +} // namespace + +class Q_DECL_HIDDEN WInputMethodHelperPrivate : public WObjectPrivate, + public WSeatKeyboardFilter { W_DECLARE_PUBLIC(WInputMethodHelper) public: @@ -86,9 +102,6 @@ class Q_DECL_HIDDEN WInputMethodHelperPrivate : public WObjectPrivate , enabledTextInput(nullptr) , activeInputMethod(nullptr) , activeKeyboardGrab(nullptr) - , keyboardGrab{} - , grabInterface{} - , handlerArg({.helper = qq, .grab = nullptr}) { Q_ASSERT(server); Q_ASSERT(seat); @@ -98,34 +111,15 @@ class Q_DECL_HIDDEN WInputMethodHelperPrivate : public WObjectPrivate Q_ASSERT(textInputManagerV3); } - void endGrab(wlr_input_method_keyboard_grab_v2 *kgv2) + bool isInputMethodVirtualKeyboard(wlr_input_method_keyboard_grab_v2 *kgv2, + WInputDevice *keyboard) const { - if (!seat) { - qCCritical(lcWlInputMethod) << "Failed to end input method keyboard grab - seat is already destroyed" - << kgv2; - return; - } - - auto *kgHandle = kgv2; - if (!kgHandle) { - qCCritical(lcWlInputMethod) << "Failed to end input method keyboard grab - grab handle is invalid" - << kgv2; - return; - } - - if (kgHandle->keyboard) { - wlr_seat_keyboard_send_modifiers(seat->handle(), &kgHandle->keyboard->modifiers); - } - // Only end the grab if our grab is still the active one on the seat. - // A popup grab may have silently replaced us (wlr_seat_keyboard_start_grab - // unconditionally overwrites keyboard_state.grab). - auto isStillActive = seat->handle()->keyboard_state.grab == &keyboardGrab; - qCDebug(lcWlInputMethod) << "endGrab: isStillActive" << isStillActive << "grab ptr" - << seat->handle()->keyboard_state.grab << "&keyboardGrab" - << &keyboardGrab; - if (isStillActive) { - wlr_seat_keyboard_end_grab(seat->handle()); - } + if (!kgv2 || !keyboard) + return false; + auto *virtualKeyboard = wlr_input_device_get_virtual_keyboard(keyboard->handle()); + return virtualKeyboard && virtualKeyboard->resource && kgv2->resource + && wl_resource_get_client(virtualKeyboard->resource) + == wl_resource_get_client(kgv2->resource); } void setKeyboard(wlr_input_method_keyboard_grab_v2 *kgv2, WInputDevice *keyboard) @@ -138,22 +132,124 @@ class Q_DECL_HIDDEN WInputMethodHelperPrivate : public WObjectPrivate } if (keyboard) { - auto *virtualKeyboard = wlr_input_device_get_virtual_keyboard(keyboard->handle()); - // refer to: - // https://github.com/swaywm/sway/blob/master/sway/input/keyboard.c#L391 - if (virtualKeyboard - && virtualKeyboard->resource - && kgHandle->resource - && wl_resource_get_client(virtualKeyboard->resource) - == wl_resource_get_client(kgHandle->resource)) { + if (isInputMethodVirtualKeyboard(kgv2, keyboard)) return; - } wlr_input_method_keyboard_grab_v2_set_keyboard(kgv2, wlr_keyboard_from_input_device(keyboard->handle())); } else { wlr_input_method_keyboard_grab_v2_set_keyboard(kgv2, nullptr); } } + bool isTextInputEligible(WTextInput *ti) const + { + if (!ti || !enabledTextInputs.contains(ti) || !seat) + return false; + + auto *focus = seat->keyboardFocusSurface(); + return focus && ti->seat() == seat && ti->focusedSurface() == focus + && ti->waylandClient() == focus->waylandClient(); + } + + // Newest enabled text input that currently matches the keyboard focus, if + // any. Activation (reconcileTextInput), key routing (keyboardGrabBypassed), + // popup anchoring (handleNewIPSV2) and commit routing (handleIMCommitted) + // are all driven by this same predicate, so they can not disagree about + // which surface currently owns the input method focus. + WTextInput *findEligibleTextInput() const + { + for (auto it = enabledTextInputs.crbegin(); it != enabledTextInputs.crend(); ++it) { + if (isTextInputEligible(*it)) + return *it; + } + return nullptr; + } + + // Announces that textInputFocusSurface() changed. Called once the focus + // state settled (end of reconcileTextInput), so consumers read a + // consistent view. Idempotent. + void notifyTextInputFocusSurfaceChanged() + { + auto *focus = enabledTextInput ? enabledTextInput->focusedSurface() : nullptr; + if (focus == lastTextInputFocusSurface) + return; + lastTextInputFocusSurface = focus; + if (!seat) { + // Teardown: no consumer is left to react. + return; + } + W_Q(WInputMethodHelper); + Q_EMIT q->textInputFocusSurfaceChanged(focus); + } + + // Wraps a native input method popup surface and announces it. Called as + // soon as a text input is eligible to anchor it. + void createPopupSurface(WSurface *focus, const QRect &cursorRect, + wlr_input_popup_surface_v2 *native); + + // Attaches pending popup surfaces to the currently eligible text input. + void drainPendingPopupSurfaces(); + + // Physical keys and modifiers are routed to the input method's keyboard + // endpoint only while the keyboard focus surface owns an eligible text + // input. The input method's activation state alone is not enough: it may + // be held across focus transitions (see reconcileTextInput), and surfaces + // that never use text-input (XWayland windows, plain terminals, games) + // must keep receiving their keys directly so that the client-side input + // method path (XIM / DBus frontends, client-side key handling) stays in + // charge. Commits follow the same rule: without an eligible text input + // there is no surface the text may be delivered to. + bool keyboardGrabBypassed(WSeat *eventSeat, WInputDevice *device) const + { + if (!seat || eventSeat != seat || !inputMethodActive + || !activeKeyboardGrab || isInputMethodVirtualKeyboard(activeKeyboardGrab, device)) { + return true; + } + if (!findEligibleTextInput()) + return true; + // Drag-and-drop owns the seat keyboard grab; keys must follow the + // drag instead of being diverted into the input method. + auto *seatHandle = seat->handle(); + return !seatHandle || seatHandle->drag != nullptr; + } + + bool filterKey(WSeat *eventSeat, WInputDevice *device, uint32_t keycode, + uint32_t state, uint32_t timestamp) override + { + if (keyboardGrabBypassed(eventSeat, device)) { + return false; + } + + setKeyboard(activeKeyboardGrab, device); + wlr_input_method_keyboard_grab_v2_send_key(activeKeyboardGrab, timestamp, keycode, state); + return true; + } + + bool filterModifiers(WSeat *eventSeat, WInputDevice *device, + const wlr_keyboard_modifiers *modifiers) override + { + if (keyboardGrabBypassed(eventSeat, device)) { + return false; + } + + auto *keyboard = wlr_keyboard_from_input_device(device->handle()); + const bool keyboardChanged = activeKeyboardGrab->keyboard != keyboard; + setKeyboard(activeKeyboardGrab, device); + if (!keyboardChanged) { + wlr_input_method_keyboard_grab_v2_send_modifiers( + activeKeyboardGrab, + const_cast(modifiers)); + } + return true; + } + + // Types text into the currently focused surface through the input + // method's virtual keyboard, by mirroring the exact server-side effect of + // a zwp_virtual_keyboard_v1 request. Returns the number of code points + // typed; code points without a keymap mapping (e.g. CJK, which has no + // physical key on any layout) are skipped, matching what any compositor + // relying on virtual-keyboard delivery can express. Never logs the text. + quint32 typeTextViaVirtualKeyboard(const QString &text); + const QPointer server; const QPointer seat; const QPointer inputMethodManagerV2; @@ -164,30 +260,181 @@ class Q_DECL_HIDDEN WInputMethodHelperPrivate : public WObjectPrivate WTextInput *enabledTextInput { nullptr }; WInputMethodV2 *activeInputMethod { nullptr }; wlr_input_method_keyboard_grab_v2 *activeKeyboardGrab {nullptr}; - - wlr_seat_keyboard_grab keyboardGrab; - wlr_keyboard_grab_interface grabInterface; - GrabHandlerArg handlerArg; - std::unique_ptr keyboardGrabListenerOwner; + bool inputMethodActive = false; + bool updatingFocus = false; + bool pendingResync = false; + // True while the input method stays activated across a momentary null + // keyboard focus (see reconcileTextInput). Keys are not routed to the + // input method in that state (there is no eligible text input, see + // keyboardGrabBypassed); the flag only records that the activation was + // deliberately kept alive instead of being torn down. + bool anchorHeld = false; + quint64 transitionSerial = 0; + // Last value announced through textInputFocusSurfaceChanged(). + WSurface *lastTextInputFocusSurface = nullptr; + WScopedListener keyboardGrabDestroyListener; QList textInputs; + QList enabledTextInputs; QList virtualKeyboards; QList popupSurfaces; + // A native popup surface the input method created before any text input + // was eligible to anchor it. The input method reuses the same popup + // surface until its panel hides, so dropping it would keep the candidate + // window invisible for that whole period; keep it until an eligible text + // input shows up, or until the input method releases it. + struct PendingPopupSurface { + wlr_input_popup_surface_v2 *handle = nullptr; + WScopedListener destroyListener; + }; + QList pendingPopupSurfaces; + }; +quint32 WInputMethodHelperPrivate::typeTextViaVirtualKeyboard(const QString &text) +{ + if (!seat || !activeKeyboardGrab || text.isEmpty()) { + return 0; + } + + // Use the newest virtual keyboard owned by the input method client; it + // carries the keymap the input method echoed from our keyboard endpoint. + auto *grabClient = wl_resource_get_client(activeKeyboardGrab->resource); + wlr_keyboard *virtualKeyboard = nullptr; + for (auto it = virtualKeyboards.crbegin(); it != virtualKeyboards.crend(); ++it) { + WInputDevice *device = *it; + if (!device || !device->handle()) { + continue; + } + auto *vk = wlr_input_device_get_virtual_keyboard(device->handle()); + if (vk && vk->has_keymap && vk->resource + && wl_resource_get_client(vk->resource) == grabClient) { + virtualKeyboard = &vk->keyboard; + break; + } + } + if (!virtualKeyboard || !virtualKeyboard->keymap || !virtualKeyboard->xkb_state) { + return 0; + } + + const KeyboardStateSnapshot snap = snapshotKeyboardState(virtualKeyboard->xkb_state); + const xkb_mod_index_t shiftIndex = + xkb_keymap_mod_get_index(virtualKeyboard->keymap, XKB_MOD_NAME_SHIFT); + const xkb_mod_mask_t shiftBit = shiftIndex == XKB_MOD_INVALID + ? xkb_mod_mask_t(0) : (xkb_mod_mask_t(1) << shiftIndex); + + const auto now = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count(); + const uint32_t timeMsec = static_cast(now); + + quint32 typed = 0; + for (int i = 0; i < text.size(); ++i) { + uint32_t codePoint = text.at(i).unicode(); + if (QChar::isHighSurrogate(codePoint) && i + 1 < text.size() + && QChar::isLowSurrogate(text.at(i + 1).unicode())) { + codePoint = QChar::surrogateToUcs4(text.at(i), text.at(i + 1)); + ++i; + } + const xkb_keysym_t keysym = codePoint < 0x100 + ? static_cast(codePoint) + : static_cast(0x01000000u + codePoint); + + xkb_keycode_t keycode = 0; + bool needsShift = false; + if (!findKeysymWithShift(virtualKeyboard->keymap, virtualKeyboard->xkb_state, + keysym, &keycode, &needsShift)) { + continue; // No physical key (e.g. CJK): cannot be typed this way. + } + + const bool shiftAlreadyDown = snap.depressed & shiftBit; + const bool pressShift = needsShift && !shiftAlreadyDown && shiftBit; + if (pressShift) { + wlr_keyboard_notify_modifiers(virtualKeyboard, snap.depressed | shiftBit, + snap.latched, snap.locked, snap.layout); + } + + // Mirror virtual_keyboard_handle_key(): the input method keeps the + // modifier state of its virtual keyboard itself, so the synthetic key + // events must not update it. wlr_keyboard_notify_key() expects evdev + // keycodes (the input method's own deliveries subtract 8 as well), + // while findKeysymWithShift() returns xkb (evdev + 8) keycodes, so + // convert before injecting. + wlr_keyboard_key_event keyEvent {}; + keyEvent.time_msec = timeMsec; + keyEvent.keycode = keycode > 8 ? keycode - 8 : keycode; + keyEvent.update_state = false; + keyEvent.state = WL_KEYBOARD_KEY_STATE_PRESSED; + wlr_keyboard_notify_key(virtualKeyboard, &keyEvent); + keyEvent.state = WL_KEYBOARD_KEY_STATE_RELEASED; + wlr_keyboard_notify_key(virtualKeyboard, &keyEvent); + + if (pressShift) { + wlr_keyboard_notify_modifiers(virtualKeyboard, snap.depressed, + snap.latched, snap.locked, snap.layout); + } + ++typed; + } + return typed; +} + +void WInputMethodHelperPrivate::createPopupSurface(WSurface *focus, const QRect &cursorRect, + wlr_input_popup_surface_v2 *native) +{ + W_Q(WInputMethodHelper); + auto surface = new WInputPopupSurface(native, focus); + popupSurfaces.append(surface); + q->updatePopupSurface(surface, cursorRect); + Q_EMIT q->inputPopupSurfaceV2Added(surface); + auto *listeners = surface->listeners(q); + listeners->add(&native->events.destroy, q, + [q, this, surface] (void *) { + popupSurfaces.removeAll(surface); + Q_EMIT q->inputPopupSurfaceV2Removed(surface); + // Safe to destroy the wrapper from inside its own destroy + // callback: the listener closure is reference-counted, and + // ~WInputPopupSurface clears the reverse mapping while the + // native popup storage is still valid. + delete surface; + }); +} + +void WInputMethodHelperPrivate::drainPendingPopupSurfaces() +{ + if (pendingPopupSurfaces.isEmpty()) + return; + + auto *ti = findEligibleTextInput(); + if (!ti || !ti->focusedSurface()) + return; + + // Take the list before wrapping anything: inputPopupSurfaceV2Added runs + // compositor code synchronously, and a re-entrant drain must not see (and + // wrap twice) entries that are already being attached. A popup created + // during this loop stays pending until the next reconcile. + const auto pending = pendingPopupSurfaces; + pendingPopupSurfaces.clear(); + for (auto *entry : pending) { + // Detach before wrapping so the native destroy signal can no longer + // free the entry from under this loop. + entry->destroyListener.disconnect(); + createPopupSurface(ti->focusedSurface(), ti->cursorRect(), entry->handle); + delete entry; + } +} + WInputMethodHelper::WInputMethodHelper(WServer *server, WSeat *seat) : QObject(server) , WObject(*new WInputMethodHelperPrivate(server, seat, this)) { W_D(WInputMethodHelper); + Q_ASSERT(!d->seat->keyboardFilter()); + d->seat->setKeyboardFilter(d); QObject::connect(d->seat, &WSeat::keyboardFocusSurfaceChanged, this, &WInputMethodHelper::resendKeyboardFocus); QObject::connect(d->seat, &WSeat::keyboardChanged, this, [d] { if (auto *activeKG = d->activeKeyboardGrab) d->setKeyboard(activeKG, d->seat->keyboard()); }); - d->seat->listeners(this)->add(&d->seat->handle()->events.keyboard_grab_begin, this, - &WInputMethodHelper::handleKeyboardGrabBegin); connect(d->inputMethodManagerV2, &WInputMethodManagerV2::newInputMethod, this, &WInputMethodHelper::handleNewIMV2); connect(d->textInputManagerV3, &WTextInputManagerV3::newTextInput, this, &WInputMethodHelper::handleNewTI); connect(d->virtualKeyboardManagerV1, &WVirtualKeyboardManagerV1::newVirtualKeyboard, this, &WInputMethodHelper::handleNewVKV1); @@ -199,6 +446,9 @@ WInputMethodHelper::~WInputMethodHelper() { teardown(); W_D(WInputMethodHelper); + d->keyboardGrabDestroyListener.disconnect(); + if (d->seat && d->seat->keyboardFilter() == d) + d->seat->setKeyboardFilter(nullptr); // The wrappers tracked below have no QObject parent and their destroy // callbacks capture this private; the native objects may outlive the // helper (which is deleted before the WServer), so release them here @@ -222,6 +472,15 @@ WInputMethodHelper::~WInputMethodHelper() delete popup; } + // Popup surfaces that never got a wrapper (no eligible text input). Only + // the tracking is dropped: the native objects stay with the input method. + const auto pendingPopupSurfaces = d->pendingPopupSurfaces; + d->pendingPopupSurfaces.clear(); + for (auto *pending : pendingPopupSurfaces) { + pending->destroyListener.disconnect(); + delete pending; + } + // Virtual keyboard device wrappers (also detach them from the seat). const auto virtualKeyboards = d->virtualKeyboards; d->virtualKeyboards.clear(); @@ -243,15 +502,6 @@ WInputMethodHelper::~WInputMethodHelper() if (d->virtualKeyboardManagerV1) d->virtualKeyboardManagerV1->disconnect(this); } -WTextInput *WInputMethodHelper::focusedTextInput() const -{ - W_DC(WInputMethodHelper); - auto focused = std::find_if(d->textInputs.begin(), d->textInputs.end(), [](WTextInput *ti) { - return ti->focusedSurface() != nullptr; - }); - return focused != d->textInputs.end() ? *focused : nullptr; -} - WTextInput *WInputMethodHelper::enabledTextInput() const { W_DC(WInputMethodHelper); @@ -299,31 +549,12 @@ void WInputMethodHelper::setInputMethod(WInputMethodV2 *im) if (d->activeInputMethod) d->activeInputMethod->removeListeners(this); d->activeInputMethod = im; + d->inputMethodActive = false; if (d->activeInputMethod) d->activeInputMethod->listeners(this)->add(&im->handle()->events.destroy, this, &WInputMethodHelper::handleActiveIMDestroyed); } -wlr_input_method_keyboard_grab_v2 *WInputMethodHelper::activeKeyboardGrab() const -{ - W_DC(WInputMethodHelper); - return d->activeKeyboardGrab; -} - -bool WInputMethodHelper::isActiveKeyboardGrabOwner() const -{ - W_DC(WInputMethodHelper); - if (!d->activeKeyboardGrab) - return false; - return d->seat->handle()->keyboard_state.grab == &d->keyboardGrab; -} - -const QList &WInputMethodHelper::virtualKeyboards() const -{ - W_DC(WInputMethodHelper); - return d->virtualKeyboards; -} - void WInputMethodHelper::handleNewIMV2(wlr_input_method_v2 *imv2) { W_D(WInputMethodHelper); @@ -332,7 +563,7 @@ void WInputMethodHelper::handleNewIMV2(wlr_input_method_v2 *imv2) // afterwards would leak it (the listeners stay registered on the native // handle and assert on its destroy). auto *imSeat = WSeat::fromHandle(imv2->seat); - if (!imSeat || d->seat->name() != imSeat->name()) + if (imSeat != d->seat) return; if (inputMethod()) { qCWarning(lcWlInputMethod) << "Ignore second creation of input on the same seat."; @@ -347,39 +578,50 @@ void WInputMethodHelper::handleNewIMV2(wlr_input_method_v2 *imv2) connect(wimv2, &WInputMethodV2::committed, this, &WInputMethodHelper::handleIMCommitted); connect(wimv2, &WInputMethodV2::newKeyboardGrab, this, &WInputMethodHelper::handleNewKGV2); connect(wimv2, &WInputMethodV2::newPopupSurface, this, &WInputMethodHelper::handleNewIPSV2); - // Once input method is online, try to resend enter to textInput - resendKeyboardFocus(); - // For text input v1, when after sendEnter, enabled signal will be emitted + qCInfo(lcWlInputMethod) << "Input method connected" + << "seat" << d->seat->name() + << "inputMethod" << wimv2; + reconcileTextInput("input method connected"); } void WInputMethodHelper::handleNewKGV2(wlr_input_method_keyboard_grab_v2 *kgv2) { W_D(WInputMethodHelper); Q_ASSERT(d->seat); - if (auto activeKG = activeKeyboardGrab()) { - d->endGrab(activeKG); + if (d->activeKeyboardGrab) { + qCWarning(lcWlInputMethod) << "Replacing an existing input method keyboard endpoint" + << "seat" << d->seat->name() + << "oldGrab" << d->activeKeyboardGrab + << "newGrab" << kgv2; + d->keyboardGrabDestroyListener.disconnect(); } + d->activeKeyboardGrab = kgv2; - d->setKeyboard(kgv2, d->seat->keyboard()); - d->grabInterface = *d->seat->handle()->keyboard_state.grab->interface; - d->grabInterface.key = handleKey; - d->grabInterface.modifiers = handleModifiers; - d->keyboardGrab.seat = d->seat->handle(); - d->handlerArg.grab = kgv2; - d->keyboardGrab.data = &d->handlerArg; - d->keyboardGrab.interface = &d->grabInterface; - wlr_seat_keyboard_start_grab(d->seat->handle(), &d->keyboardGrab); - qCDebug(lcWlInputMethod) << "IME keyboard grab installed"; - d->keyboardGrabListenerOwner = std::make_unique(); - auto *grabOwner = d->keyboardGrabListenerOwner.get(); - d->seat->listeners(grabOwner)->add(&kgv2->events.destroy, this, [this, d, kgv2, grabOwner] { - qCDebug(lcWlInputMethod) << "IME keyboard grab before_destroy"; - Q_ASSERT(activeKeyboardGrab() == kgv2); - d->endGrab(kgv2); + // Prefer the (always non-IME) keyboard-group device: fcitx5 creates its + // virtual keyboard right before the new keyboard endpoint, so at this + // point the seat's current keyboard may still be that virtual keyboard + // (which setKeyboard() would skip), leaving the new endpoint without + // keymap/repeat-info/modifiers until the first physically filtered key. + if (auto *groupKeyboard = d->seat->keyboardGroupKeyboard()) + d->setKeyboard(kgv2, groupKeyboard); + else + d->setKeyboard(kgv2, d->seat->keyboard()); + qCInfo(lcWlInputMethod) << "Input method keyboard endpoint available" + << "seat" << d->seat->name() + << "grab" << kgv2 + << "activeTextInput" << d->enabledTextInput + << "anotherSeatGrabActive" << wlr_seat_keyboard_has_grab(d->seat->handle()); + + d->keyboardGrabDestroyListener.init(&kgv2->events.destroy, this, + [d, kgv2](void *) { + if (d->activeKeyboardGrab != kgv2) + return; + qCInfo(lcWlInputMethod) << "Input method keyboard endpoint destroyed" + << "seat" << d->seat->name() + << "grab" << kgv2 + << "activeTextInput" << d->enabledTextInput; d->activeKeyboardGrab = nullptr; - d->handlerArg.grab = nullptr; - d->seat->removeListeners(grabOwner); - d->keyboardGrabListenerOwner.reset(); + d->keyboardGrabDestroyListener.disconnect(); }); } @@ -387,32 +629,37 @@ void WInputMethodHelper::handleNewIPSV2(wlr_input_popup_surface_v2 *ipsv2) { W_D(WInputMethodHelper); - auto createPopupSurface = [this, d] (WSurface *focus, QRect cursorRect, wlr_input_popup_surface_v2 *popupSurface){ - auto surface = new WInputPopupSurface(popupSurface, focus); - d->popupSurfaces.append(surface); - updatePopupSurface(surface, cursorRect); - Q_EMIT inputPopupSurfaceV2Added(surface); - auto *listeners = surface->listeners(this); - listeners->add(&popupSurface->events.destroy, this, - [this, d, surface] (void *) { - d->popupSurfaces.removeAll(surface); - Q_EMIT inputPopupSurfaceV2Removed(surface); - // Safe to destroy the wrapper from inside its own destroy - // callback: the listener closure is reference-counted, and - // ~WInputPopupSurface clears the reverse mapping while the - // native popup storage is still valid. - delete surface; + // The popup surface is anchored to the text input that currently owns the + // keyboard focus. If there is none yet (the input method asked for its + // panel surface while the focus was in flight or on a surface without a + // text input), keep it pending instead of dropping it: the input method + // reuses the same popup surface until its panel hides, so a dropped popup + // would keep the candidate window invisible for that whole period. + auto *ti = d->findEligibleTextInput(); + if (!ti || !ti->focusedSurface()) { + auto *entry = new WInputMethodHelperPrivate::PendingPopupSurface; + entry->handle = ipsv2; + entry->destroyListener.init(&ipsv2->events.destroy, this, [d, entry](void *) { + d->pendingPopupSurfaces.removeAll(entry); + entry->destroyListener.disconnect(); + delete entry; }); - }; - auto ti = enabledTextInput(); - if (ti && ti->focusedSurface()) { - createPopupSurface(ti->focusedSurface(), ti->cursorRect(), ipsv2); + d->pendingPopupSurfaces.append(entry); + qCInfo(lcWlInputMethod) << "Input method popup surface pending: no eligible text input yet" + << "seat" << (d->seat ? d->seat->name() : QString()) + << "popupSurface" << ipsv2 + << "activeTextInput" << d->enabledTextInput; + return; } + d->createPopupSurface(ti->focusedSurface(), ti->cursorRect(), ipsv2); } void WInputMethodHelper::handleNewVKV1(wlr_virtual_keyboard_v1 *vkv1) { W_D(WInputMethodHelper); + if (vkv1->seat != d->seat->handle()) + return; + auto *keyboard = new WInputDevice(&vkv1->keyboard.base, true); d->virtualKeyboards.append(keyboard); d->seat->attachInputDevice(keyboard); @@ -437,39 +684,75 @@ void WInputMethodHelper::handleNewVKV1(wlr_virtual_keyboard_v1 *vkv1) }); } -void WInputMethodHelper::handleKeyboardGrabBegin() -{ - W_D(WInputMethodHelper); - // If another grab (popup, drag, etc.) silently replaced our keyboard grab, - // notify all text inputs to leave so the IME can deactivate. - // Our grab v2 object is still alive (endGrab only runs on before_destroy), - // so activeKeyboardGrab is non-null, but seat->keyboard_state.grab no longer - // points to our keyboardGrab. - if (d->activeKeyboardGrab && d->seat->handle()->keyboard_state.grab != &d->keyboardGrab) { - qCDebug(lcWlInputMethod) << "IME keyboard grab silently replaced, notifying leave"; - notifyLeave(); - } -} - void WInputMethodHelper::resendKeyboardFocus() { W_D(WInputMethodHelper); - qCInfo(lcWlInputMethod()) << "resend keyboard focus"; auto focus = d->seat->keyboardFocusSurface(); - for (auto textInput : std::as_const(d->textInputs)) { - if (textInput->focusedSurface() && textInput->focusedSurface() != focus) - textInput->sendLeave(); - if (!focus) + if (d->updatingFocus) { + // Re-entrant request (a signal fired while reconciling focus): mark it + // pending instead of silently dropping the state change. + d->pendingResync = true; + return; + } + + d->updatingFocus = true; + qCDebug(lcWlInputMethod) << "Reconciling text-input focus" + << "seat" << d->seat->name() + << "focus" << focus + << "focusClient" << (focus ? focus->waylandClient() : nullptr) + << "textInputCount" << d->textInputs.size(); + + // Send every obsolete leave before any enter. In particular, never emit a + // same-client stale leave after a new enter: Qt's text-input-v2 client + // clears its current surface on every leave event. + // text-input-v1 has no seat before its first activate request; treat a + // null-seat text input as belonging to this seat, matching master. + for (auto *ti : std::as_const(d->textInputs)) { + if (ti->seat() && ti->seat() != d->seat) continue; - qCDebug(lcWlInputMethod()) << "trying to send focus to" << textInput << "from client" << textInput->waylandClient(); - if (focus->waylandClient() == textInput->waylandClient()) { - qCDebug(lcWlInputMethod) << "focus sent to" << textInput; - if ((!textInput->seat() || textInput->seat() == d->seat) - && textInput->focusedSurface() != focus) { - textInput->sendEnter(focus); + + auto *tiFocus = ti->focusedSurface(); + const bool clientMatches = focus && ti->waylandClient() == focus->waylandClient(); + bool shouldFocus = clientMatches; + if (qobject_cast(ti)) + shouldFocus = shouldFocus && tiFocus == focus; + + if (tiFocus && (!shouldFocus || tiFocus != focus)) + ti->sendLeave(); + } + + if (focus) { + for (auto *ti : std::as_const(d->textInputs)) { + if ((ti->seat() && ti->seat() != d->seat) + || ti->waylandClient() != focus->waylandClient()) { + continue; } + + // text-input-v1 focus is client-managed: only acknowledge surfaces + // whose activate request is still recorded. The record survives + // server-driven leave events, so keyboard focus returning to an + // activated v1 surface re-arms it here (sendEnter emits enabled() + // even before the client sees the enter event). + if (qobject_cast(ti)) { + if (ti->focusedSurface() == focus) + ti->sendEnter(focus); + continue; + } + + if (ti->focusedSurface() != focus) + ti->sendEnter(focus); } } + + d->updatingFocus = false; + reconcileTextInput("keyboard focus changed"); + + if (d->pendingResync) { + d->pendingResync = false; + qCDebug(lcWlInputMethod) << "Re-running focus reconciliation after re-entrant request" + << "seat" << d->seat->name(); + resendKeyboardFocus(); + } } void WInputMethodHelper::connectToTI(WTextInput *ti) @@ -482,16 +765,11 @@ void WInputMethodHelper::connectToTI(WTextInput *ti) void WInputMethodHelper::disableTI(WTextInput *ti) { + W_D(WInputMethodHelper); Q_ASSERT(ti); - if (enabledTextInput() == ti) { - // Should we consider the case when the same text input is disabled and then enabled at the same time. - auto im = inputMethod(); - if (im) { - im->sendDeactivate(); - im->sendDone(); - } - setEnabledTextInput(nullptr); - } + d->enabledTextInputs.removeAll(ti); + if (!d->updatingFocus) + reconcileTextInput("text input disabled"); } void WInputMethodHelper::handleNewTI(WTextInput *ti) @@ -511,72 +789,142 @@ void WInputMethodHelper::handleNewTI(WTextInput *ti) // its requestFocus signal for it might request focus from another seat to activate // itself here. For example, text input v1. connect(ti, &WTextInput::requestFocus, this, [this, ti, d]{ - if (ti->seat() && d->seat->name() == ti->seat()->name()) { + if (!ti->seat() || ti->seat() == d->seat) { connectToTI(ti); - if (auto surface = d->seat->keyboardFocusSurface()) { - ti->sendEnter(surface); - } + resendKeyboardFocus(); } }); - if (ti->seat() && d->seat->name() == ti->seat()->name()) { + if (!ti->seat() || ti->seat() == d->seat) { connectToTI(ti); - if (auto *surface = d->seat->keyboardFocusSurface(); surface - && surface->waylandClient() == ti->waylandClient()) { - ti->sendEnter(surface); + if (auto *focus = d->seat->keyboardFocusSurface(); + focus && ti->waylandClient() == focus->waylandClient()) { + ti->sendEnter(focus); } + reconcileTextInput("text input created"); } } void WInputMethodHelper::handleTIEnabled() { + W_D(WInputMethodHelper); WTextInput *ti = qobject_cast(sender()); Q_ASSERT(ti); - auto im = inputMethod(); - auto activeTI = enabledTextInput(); - if (activeTI == ti) - return; - if (activeTI) { - if (im) { - // If current active input method is not null, notify it to deactivate. - im->sendDeactivate(); - im->sendDone(); - } - // Notify last active text input to leave. - activeTI->sendLeave(); + d->enabledTextInputs.removeAll(ti); + d->enabledTextInputs.append(ti); + qCDebug(lcWlInputMethod) << "Text input became eligible candidate" + << "seat" << d->seat->name() + << "textInput" << ti + << "focusedSurface" << ti->focusedSurface() + << "seatFocus" << d->seat->keyboardFocusSurface(); + if (!d->updatingFocus) + reconcileTextInput("text input enabled"); +} + +void WInputMethodHelper::handleTIDisabled() +{ + WTextInput *ti = qobject_cast(sender()); + Q_ASSERT(ti); + disableTI(ti); +} + +void WInputMethodHelper::reconcileTextInput(const char *reason) +{ + W_D(WInputMethodHelper); + + WTextInput *candidate = d->findEligibleTextInput(); + + auto *old = d->enabledTextInput; + auto *im = d->activeInputMethod; + auto *focus = d->seat ? d->seat->keyboardFocusSurface() : nullptr; + const bool textInputChanged = old != candidate; + // Sticky anchor, restricted to the momentary null focus of a switch: while + // no surface owns the keyboard focus (window switch, popup transition, the + // compositor's own QML taking focus) keep the input method activated so + // that a focus flickering through null back to the same text input does + // not tear down and rebuild the input method's keyboard endpoint and + // virtual keyboard. As soon as the focus settles on a concrete surface + // without an eligible text input, deactivate instead: surfaces that never + // use text-input (XWayland windows, plain terminals, games) must keep + // their keys so the client-side input method path (XIM / DBus) stays in + // charge, and a held activation would leave the input method's candidate + // window parented to an unrelated text input. Only keys on an eligible + // text input are routed to the input method anyway (see + // keyboardGrabBypassed). A destroyed or client-disabled anchor (removed + // from enabledTextInputs) falls through to normal deactivation. + const bool holdAnchor = !candidate && !focus && d->inputMethodActive && old + && d->enabledTextInputs.contains(old); + const bool wasAnchorHeld = d->anchorHeld; + d->anchorHeld = holdAnchor; + const bool shouldDeactivate = d->inputMethodActive && !holdAnchor + && (!candidate || textInputChanged || !im); + bool activatedNow = false; + + if (shouldDeactivate && im) { + im->sendDeactivate(); + im->sendDone(); } - setEnabledTextInput(ti); - // Try to activate input method. - if (im) { + if (shouldDeactivate) + d->inputMethodActive = false; + + if (textInputChanged && !holdAnchor) + setEnabledTextInput(candidate); + + if (candidate && im && !d->inputMethodActive) { im->sendActivate(); - if (ti->features().testFlag(IME::F_SurroundingText)) { - im->sendSurroundingText(ti->surroundingText(), ti->surroundingCursor(), ti->surroundingAnchor()); + if (candidate->features().testFlag(IME::F_SurroundingText)) { + im->sendSurroundingText(candidate->surroundingText(), + candidate->surroundingCursor(), + candidate->surroundingAnchor()); } - im->sendTextChangeCause(ti->textChangeCause()); - if (ti->features().testFlag(IME::F_ContentType)) { - im->sendContentType(ti->contentHints().toInt(), ti->contentPurpose()); + im->sendTextChangeCause(candidate->textChangeCause()); + if (candidate->features().testFlag(IME::F_ContentType)) { + im->sendContentType(candidate->contentHints().toInt(), candidate->contentPurpose()); } im->sendDone(); + d->inputMethodActive = true; + activatedNow = true; } -} -void WInputMethodHelper::handleTIDisabled() -{ - WTextInput *ti = qobject_cast(sender()); - disableTI(ti); + if (textInputChanged || shouldDeactivate || activatedNow || (holdAnchor != wasAnchorHeld)) { + ++d->transitionSerial; + qCInfo(lcWlInputMethod) << "Input method state reconciled" + << "transition" << d->transitionSerial + << "reason" << reason + << "seat" << (d->seat ? d->seat->name() : QString()) + << "oldTextInput" << old + << "newTextInput" << candidate + << "focus" << focus + << "inputMethod" << im + << "keyboardGrab" << d->activeKeyboardGrab + << "active" << d->inputMethodActive + << "anchorHeld" << d->anchorHeld + << "candidateCount" << d->enabledTextInputs.size(); + } + + // Notify before draining: consumers re-anchor the popups that already have + // a wrapper, while popups attached here are created against the current + // focus and must not be re-anchored again in the same reconciliation. + d->notifyTextInputFocusSurfaceChanged(); + d->drainPendingPopupSurfaces(); } void WInputMethodHelper::handleFocusedTICommitted() { + W_D(WInputMethodHelper); auto ti = enabledTextInput(); - Q_ASSERT(ti); - if (!ti->focusedSurface()) { - qCWarning(lcWlInputMethod) << "Discard commit to unfocused but not disabled text input."; + if (!ti || !d->isTextInputEligible(ti)) { + qCWarning(lcWlInputMethod) << "Discard commit from ineligible text input" + << "seat" << d->seat->name() + << "textInput" << ti + << "textInputFocus" << (ti ? ti->focusedSurface() : nullptr) + << "seatFocus" << d->seat->keyboardFocusSurface(); + reconcileTextInput("commit from ineligible text input"); return; } qCDebug(lcWlInputMethod) << "Focused text input" << ti << "committed." << "Cursor rectangle:" << ti->cursorRect(); auto im = inputMethod(); - if (im) { + if (im && d->inputMethodActive) { IME::Features features = ti->features(); if (features.testFlag(IME::F_SurroundingText)) { im->sendSurroundingText(ti->surroundingText(), ti->surroundingCursor(), ti->surroundingAnchor()); @@ -593,30 +941,55 @@ void WInputMethodHelper::handleFocusedTICommitted() void WInputMethodHelper::handleIMCommitted() { + W_D(WInputMethodHelper); auto im = inputMethod(); Q_ASSERT(im); auto ti = enabledTextInput(); - if (ti && ti->focusedSurface()) { + if (d->inputMethodActive && d->isTextInputEligible(ti)) { ti->handleIMCommitted(im); + } else if (d->inputMethodActive && d->anchorHeld && d->seat + && d->seat->keyboardFocusSurface()) { + // Defensive fallback: the input method is still activated on its + // anchor while the keyboard focus already moved to a surface without + // an eligible text input. Type the commit through the input method's + // own virtual keyboard so latin/symbol text reaches the focused + // surface instead of being dropped; never route it to the unfocused + // anchor text input, and note that CJK has no keymap mapping (see + // typeTextViaVirtualKeyboard). Normal deliveries go through the + // eligible text input handled above. + const quint32 typed = d->typeTextViaVirtualKeyboard(im->commitString()); + ++d->transitionSerial; + qCInfo(lcWlInputMethod) << "Anchor-held commit delivered via virtual-keyboard typing" + << "transition" << d->transitionSerial + << "seat" << d->seat->name() + << "textInput" << ti + << "seatFocus" << d->seat->keyboardFocusSurface() + << "typedCodePoints" << typed; + if (typed == 0) { + qCDebug(lcWlInputMethod) << "Anchor-held commit could not be delivered; no typeable code point" + << "seat" << d->seat->name() + << "virtualKeyboard" << !d->virtualKeyboards.isEmpty(); + } + } else { + qCWarning(lcWlInputMethod) << "Discard input method commit without an eligible text input" + << "seat" << d->seat->name() + << "textInput" << ti + << "seatFocus" << d->seat->keyboardFocusSurface(); } } void WInputMethodHelper::handleActiveIMDestroyed() { + W_D(WInputMethodHelper); auto im = inputMethod(); Q_ASSERT(im); + qCInfo(lcWlInputMethod) << "Input method disconnected" + << "seat" << d->seat->name() + << "inputMethod" << im + << "activeTextInput" << d->enabledTextInput; setInputMethod(nullptr); delete im; - notifyLeave(); -} - -void WInputMethodHelper::notifyLeave() -{ - W_D(WInputMethodHelper); - for (auto *ti : std::as_const(d->textInputs)) { - if (ti->focusedSurface()) - ti->sendLeave(); - } + reconcileTextInput("input method disconnected"); } void WInputMethodHelper::updateAllPopupSurfaces(QRect cursorRect) diff --git a/waylib/src/server/protocols/winputmethodhelper.h b/waylib/src/server/protocols/winputmethodhelper.h index 7d939d74e0..2d57629434 100644 --- a/waylib/src/server/protocols/winputmethodhelper.h +++ b/waylib/src/server/protocols/winputmethodhelper.h @@ -31,16 +31,16 @@ class WAYLIB_SERVER_EXPORT WInputMethodHelper : public QObject, public WObject WSurface *textInputFocusSurface() const; QRect textInputCursorRect() const; - // Returns true when the seat's current keyboard grab is the one installed by this helper. - bool isActiveKeyboardGrabOwner() const; - Q_SIGNALS: void inputPopupSurfaceV2Added(WInputPopupSurface *popupSurface); void inputPopupSurfaceV2Removed(WInputPopupSurface *popupSurface); void textInputCursorRectChanged(QRect cursorRect); + // Emitted after the text input focus settled when the surface returned by + // textInputFocusSurface() changed (window switch, new anchor, commit + // routed elsewhere). Consumers may re-anchor input popup surfaces on it. + void textInputFocusSurfaceChanged(WSurface *surface); private: - const QList &virtualKeyboards() const; void handleNewTI(WTextInput *ti); void handleNewIMV2(wlr_input_method_v2 *imv2); void handleNewKGV2(wlr_input_method_keyboard_grab_v2 *kgv2); @@ -48,9 +48,8 @@ class WAYLIB_SERVER_EXPORT WInputMethodHelper : public QObject, public WObject void handleNewVKV1(wlr_virtual_keyboard_v1 *vkv1); void updateAllPopupSurfaces(QRect cursorRect); void updatePopupSurface(WInputPopupSurface *popup, QRect cursorRect); - void notifyLeave(); void resendKeyboardFocus(); - void handleKeyboardGrabBegin(); + void reconcileTextInput(const char *reason); void connectToTI(WTextInput *ti); void disableTI(WTextInput *ti); void handleTIEnabled(); @@ -58,14 +57,10 @@ class WAYLIB_SERVER_EXPORT WInputMethodHelper : public QObject, public WObject void handleFocusedTICommitted(); void handleIMCommitted(); void handleActiveIMDestroyed(); - WTextInput *focusedTextInput() const; WTextInput *enabledTextInput() const; void setEnabledTextInput(WTextInput *ti); WInputMethodV2 *inputMethod() const; void setInputMethod(WInputMethodV2 *im); - wlr_input_method_keyboard_grab_v2 *activeKeyboardGrab() const; - friend void handleKey(struct wlr_seat_keyboard_grab *grab, uint32_t time_msec, uint32_t key, uint32_t state); - friend void handleModifiers(struct wlr_seat_keyboard_grab *grab, const struct wlr_keyboard_modifiers *modifiers); }; WAYLIB_SERVER_END_NAMESPACE