From 3a79a13ec155215d72cc11fc5fef5866901cb194 Mon Sep 17 00:00:00 2001 From: groveer Date: Fri, 14 Aug 2026 13:05:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20show=20desktop=20=E2=80=94=20move=20?= =?UTF-8?q?keyboard=20focus=20to=20the=20desktop=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: showing the desktop (Super+D) did not move keyboard focus, so layer-shell windows that close on focus loss (e.g. dde-shell popups, tooltips) never exited. Also, focus was never restored to the previously active window when leaving show-desktop mode. 1. On entering show-desktop, dismiss any popup keyboard grab and move keyboard focus to the desktop layer surface for every seat; drop focus when no desktop surface exists, so focus-loss layer-shell windows exit. 2. On exit, restore keyboard focus to each seat's activated surface via the shared restoreShowDesktopFocus(). It is reached from three triggers: the protocol desktop-state change (onShowDesktop), click-activation of a window (setActivatedSurface), and shortcut/forced-activation exits (restoreFromShowDesktop). 3. Skip seats that are still in transition (hotplug) instead of asserting on a missing seat container. 4. While showing the desktop, do not yank keyboard focus back to the (hidden) activated surface when its focus capability changes or when a popup keyboard grab ends. Log: 显示桌面时键盘焦点正确转移到桌面层,退出时恢复之前的激活窗口。 Influence: 1. 在依赖失焦退出的图层窗口显示时触发 Super+D。 2. 确认该图层窗口退出,且 desktop 获得键盘焦点。 3. 再次触发 Super+D,确认焦点恢复到此前的普通窗口。 fix: 显示桌面后恢复窗口焦点 1. 显示桌面时关闭弹窗键盘抓取,将键盘焦点转移到 desktop 图层窗口。 2. 退出显示桌面时,将键盘焦点恢复到各 seat 的激活窗口。 3. 跳过热插拔切换中的 seat,避免断言。 PMS: BUG-372393 Signed-off-by: groveer --- src/seat/helper.cpp | 51 ++++++++++++++++++++++++++++++ src/seat/helper.h | 1 + src/surface/seatsurfacemanager.cpp | 26 +++++++++++++++ src/surface/seatsurfacemanager.h | 2 ++ 4 files changed, 80 insertions(+) diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index e9ebe2640..8aff53b5b 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -1468,6 +1468,48 @@ void Helper::onShowDesktop() surface->startShowDesktopAnimation(false); } } + + if (s == WindowManagementInterfaceV1::DesktopState::Show) { + // Find the desktop background surface first + SurfaceWrapper *desktopSurface = nullptr; + const auto &backgroundSurfaces = m_shellHandler->m_backgroundContainer->surfaces(); + for (SurfaceWrapper *w : backgroundSurfaces) { + auto *layer = qobject_cast(w->shellSurface()); + if (layer && layer->scope() == QStringLiteral("dde-shell/desktop")) { + desktopSurface = w; + break; + } + } + + const auto &seats = m_seatManager->seats(); + for (auto *seat : seats) { + auto *seatContainer = m_rootSurfaceContainer->getSeatContainer(seat); + // Seat may be in transition (hotplug); requestKeyboardFocus asserts + // on a missing seat container. + if (!seatContainer) + continue; + + // Dismiss any popup keyboard grab: the grab would redirect keyboard + // focus back to the popup and defeat the desktop-surface transfer. + seatContainer->dismissPopups(); + // Move keyboard focus to the desktop surface (or drop it when the + // desktop surface is unavailable) so layer-shell windows that close + // on focus loss exit. + requestKeyboardFocus(desktopSurface, Qt::OtherFocusReason, seat); + } + } else if (s == WindowManagementInterfaceV1::DesktopState::Normal) { + // m_showDesktop already set to s above; the protocol state is already Normal. + restoreShowDesktopFocus(); + } +} + +void Helper::restoreShowDesktopFocus() +{ + const auto &seats = m_seatManager->seats(); + for (auto *seat : seats) { + if (auto *seatContainer = m_rootSurfaceContainer->getSeatContainer(seat)) + seatContainer->restoreShowDesktopFocus(); + } } void Helper::onSetCopyOutput(VirtualOutputInterfaceV1 *interface) @@ -2855,6 +2897,8 @@ void Helper::setActivatedSurface(SurfaceWrapper *newActivateSurface, WSeat *seat if (oldPrimarySurface) oldPrimarySurface->setActivate(false); + bool wasShowingDesktop = false; + if (newActivateSurface) { Q_ASSERT(newActivateSurface->showOnWorkspace(workspace()->current()->id())); newActivateSurface->stackToLast(); @@ -2871,6 +2915,7 @@ void Helper::setActivatedSurface(SurfaceWrapper *newActivateSurface, WSeat *seat m_showDesktop = WindowManagementInterfaceV1::DesktopState::Normal; m_windowManagementInterfaceV1->setDesktopState(WindowManagementInterfaceV1::DesktopState::Normal); newActivateSurface->setHideByShowDesk(true); + wasShowingDesktop = true; } Q_ASSERT(newActivateSurface->hasActiveCapability()); @@ -2881,6 +2926,11 @@ void Helper::setActivatedSurface(SurfaceWrapper *newActivateSurface, WSeat *seat // it for the primary seat. Do not emit Helper::activatedSurfaceChanged again here. seatContainer->setActivatedSurface(newActivateSurface, Qt::OtherFocusReason); + // This also restores keyboard focus on the other seats; the caller's subsequent + // requestKeyboardFocus() only covers the target seat. + if (wasShowingDesktop) + restoreShowDesktopFocus(); + if (isPrimarySeat && newActivateSurface) { Q_ASSERT(newActivateSurface->hasActiveCapability()); newActivateSurface->setActivate(true); @@ -3425,6 +3475,7 @@ void Helper::restoreFromShowDesktop(SurfaceWrapper *activeSurface) surface->setSurfaceState(SurfaceWrapper::State::Minimized); } } + restoreShowDesktopFocus(); } } diff --git a/src/seat/helper.h b/src/seat/helper.h index 185b45610..f703251f1 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -372,6 +372,7 @@ private Q_SLOTS: bool isNvidiaCardPresent(); void setWorkspaceVisible(bool visible); void restoreFromShowDesktop(SurfaceWrapper *activeSurface = nullptr); + void restoreShowDesktopFocus(); void setNoAnimation(bool noAnimation); void updateSurfaceSeatInteraction(SurfaceWrapper *surface, WSeat *seat); diff --git a/src/surface/seatsurfacemanager.cpp b/src/surface/seatsurfacemanager.cpp index 7e3174408..88d336962 100644 --- a/src/surface/seatsurfacemanager.cpp +++ b/src/surface/seatsurfacemanager.cpp @@ -91,6 +91,12 @@ void SeatSurfaceManager::onActivatedSurfaceFocusCapabilityChanged() if (!helper) return; + // While showing the desktop, keyboard focus is on the desktop layer, not on the + // (hidden) activated surface; a focus-capability change of the activated + // surface must not yank keyboard focus back to the window. + if (helper->showDesktopState() == WindowManagementInterfaceV1::DesktopState::Show) + return; + if (m_activatedSurface->hasFocusCapability()) { helper->requestKeyboardFocus(m_activatedSurface, Qt::ActiveWindowFocusReason, m_seat); } else { @@ -159,6 +165,19 @@ void SeatSurfaceManager::setKeyboardFocusSurface(SurfaceWrapper *surface, Qt::Fo } } +void SeatSurfaceManager::restoreShowDesktopFocus() +{ + if (!m_activatedSurface || !m_activatedSurface->hasFocusCapability()) + return; + + // The seat may still be in transition (hotplug): without a container the + // subsequent keyboard focus request would assert. + if (!m_rootContainer || !m_rootContainer->getSeatContainer(m_seat)) + return; + + setKeyboardFocusSurface(m_activatedSurface, Qt::OtherFocusReason); +} + void SeatSurfaceManager::beginMoveResize(SurfaceWrapper *surface, Qt::Edges edges) { if (m_moveResizeState.surface) @@ -454,6 +473,13 @@ void SeatSurfaceManager::onKeyboardGrabEnd() qCDebug(lcTlPopupFocus) << "Popup keyboard grab ended, restoring focus to:" << m_activatedSurface; + // 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() == WindowManagementInterfaceV1::DesktopState::Show) + return; + } + if (m_activatedSurface && m_activatedSurface->hasFocusCapability()) { setKeyboardFocusSurface(m_activatedSurface, Qt::ActiveWindowFocusReason); } diff --git a/src/surface/seatsurfacemanager.h b/src/surface/seatsurfacemanager.h index 7e57ebcd6..848d1a35c 100644 --- a/src/surface/seatsurfacemanager.h +++ b/src/surface/seatsurfacemanager.h @@ -31,6 +31,8 @@ class SeatSurfaceManager : public QObject SurfaceWrapper *keyboardFocusSurface() const { return m_keyboardFocusSurface; } void setKeyboardFocusSurface(SurfaceWrapper *surface, Qt::FocusReason reason = Qt::OtherFocusReason); + void restoreShowDesktopFocus(); + struct MoveResizeState { SurfaceWrapper *surface = nullptr; ///< The surface being moved/resized Qt::Edges edges = Qt::Edges(); ///< Resize edges (empty for move) From c238b9aee8936534ac9c72a6fbd95e302a2d0241 Mon Sep 17 00:00:00 2001 From: groveer Date: Fri, 14 Aug 2026 13:06:06 +0800 Subject: [PATCH 2/2] fix(popup): dismiss popup grab on outside click; drop UB drag cast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: two popup focus problems. 1. Clicking outside a popup did not always dismiss it. wlroots' xdg popup grab only auto-dismisses when the clicked surface cannot receive the event (pointer focus cleared, serial == 0), i.e. clicks on other clients. Clicks on the popup client's own toplevel were delivered normally and the popup stayed open. End the grab explicitly on a mouse press that is not on an xdg popup surface, per xdg-shell semantics. 2. onKeyboardGrabBegin cast grab->data (a wlr_xdg_popup_grab*) to wlr_drag* and read grab_type from it. The first member of wlr_xdg_popup_grab is a wl_client* pointer, not an enum, so this is undefined behavior and could misclassify a popup grab as a drag, leaving m_hasPopupGrab false and breaking outside-click dismissal. seat->drag is set only AFTER keyboard_grab_begin fires (wlr_seat_start_drag), so drags cannot be detected there; exclude them at dismissal time instead. 1. Dismiss the popup keyboard grab in beforeDisposeEvent on a MouseButtonPress outside any xdg popup surface. 2. Never dismiss during an active DnD drag (seat->drag non-null): a drag also installs a keyboard grab, and ending it would break the drag. 3. Remove the undefined-behavior grab_type cast in onKeyboardGrabBegin. Log: 点击弹出窗口外部可以关闭弹出窗口;移除 DnD 抓取检测的未定义行为。 Influence: 1. 打开一个弹出窗口(如右键菜单)。 2. 点击该弹出窗口所属应用自身的窗口,确认弹出窗口关闭。 3. 点击其他应用的窗口,确认弹出窗口同样关闭且点击正常传递。 4. 拖拽文件时确认拖拽流程不受影响。 --- src/seat/helper.cpp | 21 +++++++++++++++++++++ src/surface/seatsurfacemanager.cpp | 15 --------------- src/surface/seatsurfacemanager.h | 1 + 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 8aff53b5b..84c678901 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -2463,6 +2463,27 @@ bool Helper::beforeDisposeEvent(WSeat *seat, QWindow *targetWindow, QInputEvent m_currentEventSeat = targetSeat; [[maybe_unused]] auto clearEventSeat = qScopeGuard([this] { m_currentEventSeat = nullptr; }); + // Dismiss the popup grab when the user presses a button outside the popup + // (e.g. on the desktop or another client). wlroots' xdg popup keyboard grab + // redirects keyboard focus back to the popup, so it must be ended explicitly. + if (event->type() == QEvent::MouseButtonPress) { + if (auto *seatContainer = m_rootSurfaceContainer->getSeatContainer(targetSeat)) { + if (seatContainer->hasPopupGrab()) { + auto *focused = targetSeat->handle()->pointer_state.focused_surface; + bool clickOnPopup = false; + if (focused) { + if (auto *wSurface = WSurface::fromHandle(focused)) { + if (auto *wrapper = m_rootSurfaceContainer->getSurface(wSurface)) + clickOnPopup = (wrapper->type() == SurfaceWrapper::Type::XdgPopup); + } + } + // seat->drag is non-null during an active DnD drag, which also + // installs a keyboard grab; never end that grab here. + if (!clickOnPopup && !targetSeat->handle()->drag) + seatContainer->dismissPopups(); + } + } + } if (seat == m_primarySeat) { if (event->type() == QEvent::KeyPress) { auto kevent = static_cast(event); diff --git a/src/surface/seatsurfacemanager.cpp b/src/surface/seatsurfacemanager.cpp index 88d336962..4967c75b1 100644 --- a/src/surface/seatsurfacemanager.cpp +++ b/src/surface/seatsurfacemanager.cpp @@ -444,21 +444,6 @@ void SeatSurfaceManager::onKeyboardGrabBegin() } } - // Detect DnD drag keyboard grab: - // In wlr_seat_start_drag(), drag->keyboard_grab.data = drag is set before - // wlr_seat_keyboard_start_grab() is called (which emits keyboard_grab_begin), - // but seat->drag = drag is set AFTER the grab begins. So seat->drag is still - // nullptr when this signal handler runs. Instead, cast grab->data to a - // wlr_drag pointer and validate via grab_type (enum values 0-2 are safe; - // an xdg_popup_grab's client pointer will never equal those small integers). - if (grab->data) { - auto *possibleDrag = static_cast(grab->data); - if (possibleDrag->grab_type <= WLR_DRAG_GRAB_KEYBOARD_TOUCH) { - qCDebug(lcTlPopupFocus) << "Drag keyboard grab started (not popup)"; - return; - } - } - m_hasPopupGrab = true; qCDebug(lcTlPopupFocus) << "Popup keyboard grab started"; } diff --git a/src/surface/seatsurfacemanager.h b/src/surface/seatsurfacemanager.h index 848d1a35c..6bbf006de 100644 --- a/src/surface/seatsurfacemanager.h +++ b/src/surface/seatsurfacemanager.h @@ -64,6 +64,7 @@ class SeatSurfaceManager : public QObject // Popup keyboard grab management void givePopupFocus(SurfaceWrapper *popupWrapper); void dismissPopups(); + bool hasPopupGrab() const { return m_hasPopupGrab; } Q_SIGNALS: void activatedSurfaceChanged(SurfaceWrapper *surface);