Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions frame/qml/PanelPopup.qml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Item {
property var popupWindow: Panel.popupWindow
property int popupX: 0
property int popupY: 0
property bool closeOnInactive: true
property bool readyBinding: false
property bool openPending: false
property bool contentReady: true
Expand Down Expand Up @@ -112,6 +113,15 @@ Item {
popupWindow.currentItem = null
}

onCloseOnInactiveChanged: {
if (!closeOnInactive) return
Qt.callLater(function() {
if (control.closeOnInactive && popupWindow && readyBinding
&& popupWindow.currentItem === control && !popupWindow.active)
control.close()
})
}

onContentReadyChanged: {
if (!contentReady || !contentOpenPending)
return
Expand Down Expand Up @@ -150,7 +160,7 @@ Item {
if (!popupWindow || !readyBinding || popupWindow.currentItem !== control || !popup.visible) {
return
}
if (!popupWindow.active) {
if (control.closeOnInactive && !popupWindow.active) {
control.close()
}
}
Expand All @@ -176,7 +186,7 @@ Item {
return
}
Qt.callLater(function() {
if (!popupWindow
if (!control.closeOnInactive || !popupWindow
|| popupWindow.active) {
return
}
Expand Down Expand Up @@ -224,7 +234,7 @@ Item {
|| control.grabInactivePending) {
return
}
if (!popupWindow.active) {
if (control.closeOnInactive && !popupWindow.active) {
control.close()
}
})
Expand All @@ -236,7 +246,7 @@ Item {
visible: readyBinding
width: control.width
height: control.height
parent: popupWindow ? popupWindow.contentItem : undefined
parent: popupWindow ? popupWindow.contentItem : null
}
Component.onDestruction: {
if (popup.visible)
Expand Down
22 changes: 22 additions & 0 deletions panels/dock/dockpositioner.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
#include "dockpositioner.h"
#include <panel.h>

#include <QLoggingCategory>

Check warning on line 8 in panels/dock/dockpositioner.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QLoggingCategory> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QQuickItem>

Check warning on line 9 in panels/dock/dockpositioner.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QQuickItem> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QQuickWindow>

Check warning on line 10 in panels/dock/dockpositioner.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QQuickWindow> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QScreen>

Check warning on line 11 in panels/dock/dockpositioner.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QScreen> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QTimer>

Check warning on line 12 in panels/dock/dockpositioner.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QTimer> not found. Please note: Cppcheck does not need standard library headers to get proper results.

DS_USE_NAMESPACE

Expand All @@ -34,6 +35,10 @@
connect(m_positionTimer, &QTimer::timeout, this, &DockPositioner::updatePosition);

Q_ASSERT(m_panel);
if (auto window = m_panel->window()) {
connect(window, &QWindow::screenChanged, this, &DockPositioner::setScreen);
setScreen(window->screen());
}
connect(m_panel, SIGNAL(positionChanged(Position)), this, SLOT(update()));
connect(m_panel, SIGNAL(geometryChanged(QRect)), this, SLOT(update()));
// The popup may be opened in the same event-loop turn as its anchor is
Expand Down Expand Up @@ -78,6 +83,23 @@
return m_y;
}

void DockPositioner::setScreen(QScreen *screen)
{
if (m_screen == screen) return;
if (m_screen) disconnect(m_screen, nullptr, this, nullptr);
m_screen = screen;
if (m_screen) {
connect(m_screen, &QScreen::availableGeometryChanged, this, &DockPositioner::availableGeometryChanged);
connect(m_screen, &QObject::destroyed, this, &DockPositioner::availableGeometryChanged);
}
emit availableGeometryChanged();
}

QRect DockPositioner::availableGeometry() const
{
return m_screen ? m_screen->availableGeometry() : QRect();
}

QRect DockPositioner::dockGeometry() const
{
return m_panel ? m_panel->property("geometry").toRect() : QRect();
Expand Down
7 changes: 6 additions & 1 deletion panels/dock/dockpositioner.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

Expand All @@ -23,6 +23,7 @@ class DockPositioner : public QObject
Q_PROPERTY(QRect bounding READ bounding WRITE setBounding NOTIFY boundingChanged FINAL)
Q_PROPERTY(int x READ x NOTIFY xChanged FINAL)
Q_PROPERTY(int y READ y NOTIFY yChanged FINAL)
Q_PROPERTY(QRect availableGeometry READ availableGeometry NOTIFY availableGeometryChanged FINAL)
QML_NAMED_ELEMENT(DockPositioner)
QML_UNCREATABLE("DockPositioner is only available via attached properties.")
QML_ATTACHED(DockPositioner)
Expand All @@ -37,6 +38,7 @@ class DockPositioner : public QObject
int x() const;
int y() const;
QRect dockGeometry() const;
QRect availableGeometry() const;
Position dockPosition() const;

void setX(int x);
Expand All @@ -49,8 +51,11 @@ public slots:
void boundingChanged();
void xChanged();
void yChanged();
void availableGeometryChanged();

protected:
void setScreen(QScreen *screen);
QPointer<QScreen> m_screen;
QPointer<DS_NAMESPACE::DPanel> m_panel;
QRect m_bounding {};
int m_x = 0;
Expand Down
16 changes: 16 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@ add_custom_target(coverage
DEPENDS frame_coverage applets_coverage dock_coverage
COMMENT "Run all dde-shell module tests and generate coverage reports"
)

# QML lifecycle and folder component smoke tests. No desktop session or app
# manager is started; native window interactions are covered by manual testing.
find_program(QML_TEST_RUNNER NAMES qmltestrunner6 qmltestrunner
HINTS /usr/lib/qt${QT_VERSION_MAJOR}/bin)
if(QML_TEST_RUNNER)
foreach(_suite frame/qml)
string(REPLACE "/" "_" _name "${_suite}")
add_test(NAME ${_name}
COMMAND ${CMAKE_COMMAND} -E env
"QT_QPA_PLATFORM=offscreen" "QT_QUICK_BACKEND=software"
"LD_PRELOAD=$<TARGET_FILE:dde-shell-frame>:$<TARGET_FILE:dde-shell-dock>"
${QML_TEST_RUNNER} -input "${CMAKE_CURRENT_SOURCE_DIR}/${_suite}"
-import "${CMAKE_BINARY_DIR}/plugins")
endforeach()
endif()
135 changes: 135 additions & 0 deletions tests/frame/qml/tst_panelpopup.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick
import QtTest
import "../../../frame/qml" as Shell

TestCase {
name: "PanelPopupLifecycle"
when: windowShown
// Control focus explicitly: these tests exercise popup policy, not the WM.
QtObject {
id: sharedWindow
property var currentItem: null
property Item contentItem: Item {}
property real requestedWidth: 120
property real requestedHeight: 80
property real xOffset: 0
property real yOffset: 0
property bool visible: false
property bool active: false
property real opacity: 1
property string title: ""
property bool x11GrabFocusTransition: false
signal updateGeometryFinished()
signal frameSwapped()
signal x11FocusOutByGrab()
signal x11FocusInByUngrab()
function requestUpdateGeometry() { updateGeometryFinished() }
function show() { visible = true }
function close() { visible = false; active = false }
function requestActivate() { active = true }
function update() { Qt.callLater(function() { sharedWindow.frameSwapped() }) }
}
Shell.PanelPopup {
id: first
popupWindow: sharedWindow
width: 120; height: 80
grabInactiveTimeout: 20
Rectangle { anchors.fill: parent; color: "red" }
}
Shell.PanelPopup {
id: second
popupWindow: sharedWindow
width: 140; height: 90
}
function cleanup() {
first.close()
second.close()
first.closeOnInactive = true
second.closeOnInactive = true
first.revealAfterRender = false
wait(0)
}
function test_openCloseAndToggle() {
first.open()
tryCompare(sharedWindow, "visible", true)
compare(sharedWindow.opacity, 1)
compare(sharedWindow.currentItem, first)
first.open() // A second click closes immediately, without an animation.
compare(sharedWindow.currentItem, null)
compare(sharedWindow.visible, false)
compare(sharedWindow.opacity, 1)
}
function test_switchSharedWindow() {
first.open()
tryCompare(sharedWindow, "visible", true)
second.open()
tryCompare(sharedWindow, "currentItem", second)
tryCompare(sharedWindow, "visible", true)
compare(sharedWindow.opacity, 1)
// A stale close from the previous folder must not close the new owner.
first.close()
compare(sharedWindow.currentItem, second)
compare(sharedWindow.visible, true)
}
function test_revealAfterRender() {
first.revealAfterRender = true
first.open()
tryCompare(sharedWindow, "visible", true)
tryCompare(first, "revealPending", false)
compare(sharedWindow.opacity, 1)
compare(sharedWindow.active, true)
first.close()
compare(sharedWindow.visible, false)
compare(sharedWindow.currentItem, null)
}
function test_focusLoss_data() {
let cases = []
for (const source of ["activeChanged", "grabTimeout", "ungrab"]) {
for (const close of [true, false])
cases.push({tag: source + "-" + close, source: source, close: close})
}
return cases
}
function test_focusLoss(data) {
first.closeOnInactive = data.close
first.open()
tryCompare(sharedWindow, "visible", true)
compare(sharedWindow.active, true)
if (data.source !== "activeChanged") sharedWindow.x11FocusOutByGrab()
sharedWindow.active = false
if (data.source === "ungrab") sharedWindow.x11FocusInByUngrab()
if (data.close) {
tryCompare(sharedWindow, "currentItem", null)
compare(sharedWindow.visible, false)
} else {
if (data.source === "grabTimeout") tryCompare(first, "grabInactivePending", false)
wait(30) // Process deferred focus callbacks before checking visibility.
compare(sharedWindow.currentItem, first)
compare(sharedWindow.visible, true)
}
}
function test_interactionEndsWhileInactive() {
first.closeOnInactive = false
first.open()
tryCompare(sharedWindow, "visible", true)
sharedWindow.active = false
wait(30)
compare(sharedWindow.visible, true)
first.closeOnInactive = true
tryCompare(sharedWindow, "currentItem", null)
compare(sharedWindow.visible, false)
}
function test_interactionEndsAfterFocusReturns() {
first.closeOnInactive = false
first.open()
tryCompare(sharedWindow, "visible", true)
sharedWindow.active = false
first.closeOnInactive = true
sharedWindow.active = true
wait(30)
compare(sharedWindow.currentItem, first)
compare(sharedWindow.visible, true)
}
}
Loading