From b5ab25a378eaff33cb69b401b2d63b4266f42a01 Mon Sep 17 00:00:00 2001 From: Ivy233 Date: Wed, 1 Apr 2026 15:50:21 +0800 Subject: [PATCH] fix: guard PanelPopup close behavior against transient X11 grab focus changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复:防止PanelPopup在X11键盘抓取期间因瞬时失焦被误关闭 1. Detect X11 FocusOut(GRAB)/FocusIn(UNGRAB) in PopupWindow and expose grab-transition signals. 2. Keep close-on-inactive behavior, but defer close during grab transition and cancel when ungrab focus returns. 3. Handle active-state transitions only for the current popup owner instance to avoid cross-instance interference. 1. 在PopupWindow中识别X11的FocusOut(GRAB)/FocusIn(UNGRAB),并向QML暴露抓取过渡信号。 2. 保留失焦关闭语义,但在抓取过渡期间延迟关闭,并在Ungrab后焦点恢复时取消关闭。 3. 仅由当前popup owner实例处理active状态变化,避免共享popupWindow下多个实例互相干扰。 PMS: BUG-301109 --- frame/popupwindow.cpp | 46 +++++++++++++++++++++++++++++ frame/popupwindow.h | 10 +++++++ frame/qml/PanelPopup.qml | 64 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/frame/popupwindow.cpp b/frame/popupwindow.cpp index 95e911efc..a7425f9b9 100644 --- a/frame/popupwindow.cpp +++ b/frame/popupwindow.cpp @@ -4,6 +4,8 @@ #include "popupwindow.h" +#include + DS_BEGIN_NAMESPACE PopupWindow::PopupWindow(QWindow *parent) : QQuickApplicationWindow(parent) @@ -22,6 +24,12 @@ PopupWindow::PopupWindow(QWindow *parent) connect(this, &QWindow::screenChanged, this, setMaximumSize); setMaximumSize(); + + connect(this, &QWindow::visibleChanged, this, [this]() { + if (!isVisible()) { + setX11GrabFocusTransition(false); + } + }); } void PopupWindow::mouseReleaseEvent(QMouseEvent *event) @@ -49,4 +57,42 @@ void PopupWindow::mouseMoveEvent(QMouseEvent *event) return QQuickApplicationWindow::mouseMoveEvent(event); } +void PopupWindow::setX11GrabFocusTransition(bool transition) +{ + if (m_x11GrabFocusTransition == transition) { + return; + } + + m_x11GrabFocusTransition = transition; + Q_EMIT x11GrabFocusTransitionChanged(); +} + +bool PopupWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) +{ + if (eventType == QByteArrayLiteral("xcb_generic_event_t")) { + auto *event = static_cast(message); + if (!event) { + return QQuickApplicationWindow::nativeEvent(eventType, message, result); + } + + const uint8_t responseType = event->response_type & ~0x80; + if (responseType == XCB_FOCUS_IN || responseType == XCB_FOCUS_OUT) { + auto *focusEvent = reinterpret_cast(event); + if (focusEvent->event == static_cast(winId())) { + if (responseType == XCB_FOCUS_OUT && focusEvent->mode == XCB_NOTIFY_MODE_GRAB) { + setX11GrabFocusTransition(true); + Q_EMIT x11FocusOutByGrab(); + } else if (responseType == XCB_FOCUS_IN && focusEvent->mode == XCB_NOTIFY_MODE_UNGRAB) { + Q_EMIT x11FocusInByUngrab(); + setX11GrabFocusTransition(false); + } else if (responseType == XCB_FOCUS_IN && focusEvent->mode != XCB_NOTIFY_MODE_GRAB) { + setX11GrabFocusTransition(false); + } + } + } + } + + return QQuickApplicationWindow::nativeEvent(eventType, message, result); +} + DS_END_NAMESPACE diff --git a/frame/popupwindow.h b/frame/popupwindow.h index cf68dbe9a..5c9925446 100644 --- a/frame/popupwindow.h +++ b/frame/popupwindow.h @@ -12,18 +12,28 @@ class PopupWindow : public QQuickApplicationWindow { Q_OBJECT Q_PROPERTY(QWindow *transientParent READ transientParent WRITE setTransientParent NOTIFY transientParentChanged) + Q_PROPERTY(bool x11GrabFocusTransition READ x11GrabFocusTransition NOTIFY x11GrabFocusTransitionChanged) QML_NAMED_ELEMENT(PopupWindow) public: PopupWindow(QWindow *parent = nullptr); + bool x11GrabFocusTransition() const { return m_x11GrabFocusTransition; } protected: void mouseReleaseEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; + bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override; + +signals: + void x11GrabFocusTransitionChanged(); + void x11FocusOutByGrab(); + void x11FocusInByUngrab(); private: + void setX11GrabFocusTransition(bool transition); bool m_dragging; bool m_pressing; + bool m_x11GrabFocusTransition = false; }; DS_END_NAMESPACE diff --git a/frame/qml/PanelPopup.qml b/frame/qml/PanelPopup.qml index 8987df717..b01c69b82 100644 --- a/frame/qml/PanelPopup.qml +++ b/frame/qml/PanelPopup.qml @@ -15,6 +15,8 @@ Item { property int popupX: 0 property int popupY: 0 property bool readyBinding: false + property bool grabInactivePending: false + property int grabInactiveTimeout: 200 // WM_NAME, used for kwin. property string windowTitle: "dde-shell/panelpopup" width: popup.childrenRect.width @@ -84,8 +86,24 @@ Item { popupWindow.requestActivate() } } + Timer { + id: grabInactiveTimer + interval: control.grabInactiveTimeout + repeat: false + onTriggered: { + control.grabInactivePending = false + if (!popupWindow || !readyBinding || popupWindow.currentItem !== control || !popup.visible) { + return + } + if (!popupWindow.active) { + control.close() + } + } + } function close() { + grabInactivePending = false + grabInactiveTimer.stop() if (!popupWindow) return @@ -103,11 +121,55 @@ Item { { if (!popupWindow) return + if (popupWindow.currentItem !== control || !popup.visible) { + control.grabInactivePending = false + grabInactiveTimer.stop() + return + } + if (popupWindow.active) { + control.grabInactivePending = false + grabInactiveTimer.stop() + return + } + if (control.grabInactivePending || popupWindow.x11GrabFocusTransition) { + return + } // TODO why activeChanged is not emit. - if (popupWindow && !popupWindow.active) { + if (!popupWindow.active) { control.close() } } + + function onX11FocusOutByGrab() + { + if (!popupWindow || !readyBinding || !popup.visible || popupWindow.currentItem !== control) { + return + } + control.grabInactivePending = true + grabInactiveTimer.start() + } + + function onX11FocusInByUngrab() + { + if (!popupWindow || popupWindow.currentItem !== control || !control.grabInactivePending) { + return + } + control.grabInactivePending = false + grabInactiveTimer.stop() + + Qt.callLater(function() { + if (!popupWindow + || !readyBinding + || popupWindow.currentItem !== control + || !popup.visible + || control.grabInactivePending) { + return + } + if (!popupWindow.active) { + control.close() + } + }) + } } Item {