From fb6ce3707a5686a89d24b04887e858e5b1372c83 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Mon, 18 Mar 2024 23:28:07 +0200 Subject: [PATCH] autotests: Add _NET_WM_STATE_MODAL tests --- autotests/integration/x11_window_test.cpp | 283 ++++++++++++++++++++++ src/window.cpp | 5 + src/window.h | 1 + src/x11window.cpp | 10 +- src/x11window.h | 1 + 5 files changed, 298 insertions(+), 2 deletions(-) diff --git a/autotests/integration/x11_window_test.cpp b/autotests/integration/x11_window_test.cpp index 2a1eeeeace..b252820430 100644 --- a/autotests/integration/x11_window_test.cpp +++ b/autotests/integration/x11_window_test.cpp @@ -100,6 +100,12 @@ private Q_SLOTS: void testCaptionWmName(); void testActivateFocusedWindow(); void testReentrantMoveResize(); + void testModal(); + void testGroupModal(); + void testCloseModal(); + void testCloseInactiveModal(); + void testCloseGroupModal(); + void testCloseInactiveGroupModal(); }; void X11WindowTest::initTestCase_data() @@ -2498,5 +2504,282 @@ void X11WindowTest::testReentrantMoveResize() QVERIFY(Test::waitForWindowClosed(window)); } +void X11WindowTest::testModal() +{ + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), QRect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QVERIFY(!child->isModal()); + QCOMPARE(child->transientFor(), parent); + + // Set modal state. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(child, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + + // Unset modal state. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::Modal); + xcb_flush(c.get()); + } + QVERIFY(modalChangedSpy.wait()); + QVERIFY(!child->isModal()); + + // Set modal state and try to activate the parent window, it should not succeed. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + workspace()->activateWindow(parent); + QCOMPARE(workspace()->activeWindow(), child); + + // It should be okay to activate an unrelated window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), QRect(0, 0, 100, 200)); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testGroupModal() +{ + // This test verifies that a dialog can be modal to the window group. + + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), QRect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + + // Set modal state. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(dialog, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(dialog->isModal()); + + // Unset modal state. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::State(), NET::Modal); + xcb_flush(c.get()); + } + QVERIFY(modalChangedSpy.wait()); + QVERIFY(!dialog->isModal()); + + // Set modal state and try to activate other windows in the group, it should not succeed. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QVERIFY(modalChangedSpy.wait()); + QVERIFY(dialog->isModal()); + workspace()->activateWindow(leader); + QCOMPARE(workspace()->activeWindow(), dialog); + workspace()->activateWindow(follower); + QCOMPARE(workspace()->activeWindow(), dialog); + + // It should be okay to activate an unrelated window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), QRect(0, 0, 100, 200)); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testCloseModal() +{ + // This test verifies that the parent window will be activated when an active modal dialog is closed. + + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), QRect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QVERIFY(!child->isModal()); + QCOMPARE(child->transientFor(), parent); + + // Set modal state. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(child, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + QCOMPARE(workspace()->activeWindow(), child); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + xcb_unmap_window(c.get(), child->window()); + xcb_destroy_window(c.get(), child->window()); + xcb_flush(c.get()); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), parent); +} + +void X11WindowTest::testCloseInactiveModal() +{ + // This test verifies that the parent window will not be activated when an inactive modal dialog is closed. + + // Create a parent and a child windows. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *parent = createWindow(c.get(), QRect(0, 0, 100, 200)); + X11Window *child = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &parent](xcb_window_t windowId) { + xcb_icccm_set_wm_transient_for(c.get(), windowId, parent->window()); + }); + QVERIFY(!child->isModal()); + QCOMPARE(child->transientFor(), parent); + + // Set modal state. + { + NETWinInfo info(c.get(), child->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(child, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(child->isModal()); + QCOMPARE(workspace()->activeWindow(), child); + + // Show another window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), QRect(0, 0, 100, 200)); + QCOMPARE(workspace()->activeWindow(), unrelated); + + // Close the child. + QSignalSpy childClosedSpy(child, &Window::closed); + xcb_unmap_window(c.get(), child->window()); + xcb_destroy_window(c.get(), child->window()); + xcb_flush(c.get()); + QVERIFY(childClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + +void X11WindowTest::testCloseGroupModal() +{ + // This test verifies that when an active modal group dialog is closed, the focus will be passed to one of its main windows. + + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), QRect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + + // Set modal state. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(dialog, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(dialog->isModal()); + QCOMPARE(workspace()->activeWindow(), dialog); + + // Close the dialog. + QSignalSpy dialogClosedSpy(dialog, &Window::closed); + xcb_unmap_window(c.get(), dialog->window()); + xcb_destroy_window(c.get(), dialog->window()); + xcb_flush(c.get()); + QVERIFY(dialogClosedSpy.wait()); + QVERIFY(workspace()->activeWindow() == leader || workspace()->activeWindow() == follower); +} + +void X11WindowTest::testCloseInactiveGroupModal() +{ + // This test verifies that when an inactive modal group dialog is closed, the focus will not be passed to one of its main windows. + + // Create the leader, a follower and a dialog window. + Test::XcbConnectionPtr c = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *leader = createWindow(c.get(), QRect(0, 0, 100, 200), [&c](xcb_window_t windowId) { + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &windowId); + }); + X11Window *follower = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + }); + X11Window *dialog = createWindow(c.get(), QRect(0, 0, 100, 200), [&c, &leader](xcb_window_t windowId) { + const xcb_window_t leaderId = leader->window(); + xcb_change_property(c.get(), XCB_PROP_MODE_REPLACE, windowId, atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &leaderId); + xcb_icccm_set_wm_transient_for(c.get(), windowId, kwinApp()->x11RootWindow()); + }); + QVERIFY(dialog->isTransient()); + QVERIFY(leader->hasTransient(dialog, true)); + QVERIFY(follower->hasTransient(dialog, true)); + + // Set modal state. + { + NETWinInfo info(c.get(), dialog->window(), kwinApp()->x11RootWindow(), NET::WMState, NET::Properties2()); + info.setState(NET::Modal, NET::Modal); + xcb_flush(c.get()); + } + QSignalSpy modalChangedSpy(dialog, &Window::modalChanged); + QVERIFY(modalChangedSpy.wait()); + QVERIFY(dialog->isModal()); + QCOMPARE(workspace()->activeWindow(), dialog); + + // Show another window. + Test::XcbConnectionPtr c1 = Test::createX11Connection(); + QVERIFY(!xcb_connection_has_error(c.get())); + X11Window *unrelated = createWindow(c1.get(), QRect(0, 0, 100, 200)); + QCOMPARE(workspace()->activeWindow(), unrelated); + + // Close the dialog. + QSignalSpy dialogClosedSpy(dialog, &Window::closed); + xcb_unmap_window(c.get(), dialog->window()); + xcb_destroy_window(c.get(), dialog->window()); + xcb_flush(c.get()); + QVERIFY(dialogClosedSpy.wait()); + QCOMPARE(workspace()->activeWindow(), unrelated); +} + WAYLANDTEST_MAIN(X11WindowTest) #include "x11_window_test.moc" diff --git a/src/window.cpp b/src/window.cpp index 794fa1ca36..b76b8692e2 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -2266,6 +2266,7 @@ void Window::setModal(bool m) return; } m_modal = m; + doSetModal(); Q_EMIT modalChanged(); // Changing modality for a mapped window is weird (?) // _NET_WM_STATE_MODAL should possibly rather be _NET_WM_WINDOW_TYPE_MODAL_DIALOG @@ -4321,6 +4322,10 @@ void Window::doSetSuspended() { } +void Window::doSetModal() +{ +} + } // namespace KWin #include "moc_window.cpp" diff --git a/src/window.h b/src/window.h index 51525338cb..03bcc8e76a 100644 --- a/src/window.h +++ b/src/window.h @@ -1520,6 +1520,7 @@ protected: virtual void doSetHidden(); virtual void doSetHiddenByShowDesktop(); virtual void doSetSuspended(); + virtual void doSetModal(); void setupWindowManagementInterface(); void destroyWindowManagementInterface(); diff --git a/src/x11window.cpp b/src/x11window.cpp index bfb6d758a9..0cf7bdac13 100644 --- a/src/x11window.cpp +++ b/src/x11window.cpp @@ -418,7 +418,6 @@ void X11Window::releaseWindow(bool on_shutdown) // and repareting to root an atomic operation (https://lists.kde.org/?l=kde-devel&m=116448102901184&w=2) grabXServer(); exportMappingState(XCB_ICCCM_WM_STATE_WITHDRAWN); - setModal(false); // Otherwise its mainwindow wouldn't get focus if (!on_shutdown) { workspace()->activateNextWindow(this); } @@ -492,7 +491,6 @@ void X11Window::destroyWindow() } finishWindowRules(); blockGeometryUpdates(); - setModal(false); workspace()->activateNextWindow(this); cleanGrouping(); workspace()->removeX11Window(this); @@ -2187,6 +2185,11 @@ void X11Window::doSetHiddenByShowDesktop() updateVisibility(); } +void X11Window::doSetModal() +{ + info->setState(isModal() ? NET::Modal : NET::States(), NET::Modal); +} + void X11Window::doSetOnActivities(const QStringList &activityList) { #if KWIN_BUILD_ACTIVITIES @@ -3475,6 +3478,9 @@ QList X11Window::mainWindows() const Window *X11Window::findModal(bool allow_itself) { + if (isDeleted()) { + return nullptr; + } for (auto it = transients().constBegin(); it != transients().constEnd(); ++it) { if (Window *ret = (*it)->findModal(true)) { return ret; diff --git a/src/x11window.h b/src/x11window.h index ec7102459c..0ec33fb4af 100644 --- a/src/x11window.h +++ b/src/x11window.h @@ -342,6 +342,7 @@ protected: void doSetDemandsAttention() override; void doSetHidden() override; void doSetHiddenByShowDesktop() override; + void doSetModal() override; bool belongsToDesktop() const override; bool doStartInteractiveMoveResize() override; bool isWaitingForInteractiveResizeSync() const override;