autotests: Add _NET_WM_STATE_MODAL tests

This commit is contained in:
Vlad Zahorodnii 2024-03-18 23:28:07 +02:00
parent 3ebe34bc3f
commit fb6ce3707a
5 changed files with 298 additions and 2 deletions

View file

@ -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"

View file

@ -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"

View file

@ -1520,6 +1520,7 @@ protected:
virtual void doSetHidden();
virtual void doSetHiddenByShowDesktop();
virtual void doSetSuspended();
virtual void doSetModal();
void setupWindowManagementInterface();
void destroyWindowManagementInterface();

View file

@ -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<Window *> 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;

View file

@ -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;