diff --git a/autotests/integration/stacking_order_test.cpp b/autotests/integration/stacking_order_test.cpp --- a/autotests/integration/stacking_order_test.cpp +++ b/autotests/integration/stacking_order_test.cpp @@ -51,13 +51,16 @@ void cleanup(); void testTransientIsAboveParent(); + void testLowerTransient(); void testRaiseTransient(); void testDeletedTransient(); void testGroupTransientIsAboveWindowGroup(); + void testLowerGroupTransient(); void testRaiseGroupTransient(); void testDeletedGroupTransient(); void testDontKeepAboveNonModalDialogGroupTransients(); + void testDontLowerWindowGroup(); void testDontRaiseWindowGroup(); }; @@ -133,6 +136,68 @@ QCOMPARE(workspace()->stackingOrder(), (ToplevelList{parent, transient})); } +void StackingOrderTest::testLowerTransient() +{ + // This test verifies that the parent and the transient are lowered together. + + // Create the parent. + KWayland::Client::Surface *parentSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(parentSurface); + KWayland::Client::ShellSurface *parentShellSurface = + Test::createShellSurface(parentSurface, parentSurface); + QVERIFY(parentShellSurface); + ShellClient *parent = Test::renderAndWaitForShown(parentSurface, QSize(256, 256), Qt::blue); + QVERIFY(parent); + QVERIFY(parent->isActive()); + QVERIFY(!parent->isTransient()); + + // Initially, the stacking order should contain only the parent window. + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{parent})); + + // Create the transient. + KWayland::Client::Surface *transientSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(transientSurface); + KWayland::Client::ShellSurface *transientShellSurface = + Test::createShellSurface(transientSurface, transientSurface); + QVERIFY(transientShellSurface); + transientShellSurface->setTransient(parentSurface, QPoint(0, 0)); + ShellClient *transient = Test::renderAndWaitForShown( + transientSurface, QSize(128, 128), Qt::red); + QVERIFY(transient); + QTRY_VERIFY(transient->isActive()); + QVERIFY(transient->isTransient()); + + // The transient should be above the parent. + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{parent, transient})); + + // Create a window that doesn't have any relationship to the parent or the transient. + KWayland::Client::Surface *anotherSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(anotherSurface); + KWayland::Client::ShellSurface *anotherShellSurface = + Test::createShellSurface(anotherSurface, anotherSurface); + QVERIFY(anotherShellSurface); + ShellClient *anotherClient = Test::renderAndWaitForShown(anotherSurface, QSize(128, 128), Qt::green); + QVERIFY(anotherClient); + QVERIFY(anotherClient->isActive()); + QVERIFY(!anotherClient->isTransient()); + + // The newly created surface has to be above both the parent and the transient. + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{parent, transient, anotherClient})); + + // Bring the transient and the parent to the initial test position. + workspace()->activateClient(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{anotherClient, parent, transient})); + + // Lower the transient, parent should be lowered too. + workspace()->lowerClient(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{parent, transient, anotherClient})); +} + void StackingOrderTest::testRaiseTransient() { // This test verifies that both the parent and the transient will be @@ -467,6 +532,130 @@ QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1, member2, transient})); } +void StackingOrderTest::testLowerGroupTransient() +{ + // This test verifies that members of a window group are lowered together + // with group transient. + + const QRect geometry = QRect(0, 0, 128, 128); + + QScopedPointer conn( + xcb_connect(nullptr, nullptr)); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry); + xcb_map_window(conn.data(), leaderWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + Client *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->windowId(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member1Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + Client *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->windowId(), member1Wid); + QCOMPARE(member1->group(), leader->group()); + QVERIFY(!member1->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1})); + + // Create yet another group member. + windowCreatedSpy.clear(); + xcb_window_t member2Wid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member2Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + Client *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->windowId(), member2Wid); + QCOMPARE(member2->group(), leader->group()); + QVERIFY(!member2->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1, member2})); + + // Create a group transient. + windowCreatedSpy.clear(); + xcb_window_t transientWid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.data(), transientWid, rootWindow()); + + // Currently, we have some weird bug workaround: if a group transient + // is a non-modal dialog, then it won't be kept above its window group. + // We need to explicitly specify window type, otherwise the window type + // will be deduced to _NET_WM_WINDOW_TYPE_DIALOG because we set transient + // for before (the EWMH spec says to do that). + xcb_atom_t net_wm_window_type = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE"), false, conn.data()); + xcb_atom_t net_wm_window_type_normal = Xcb::Atom( + QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.data()); + xcb_change_property( + conn.data(), // c + XCB_PROP_MODE_REPLACE, // mode + transientWid, // window + net_wm_window_type, // property + XCB_ATOM_ATOM, // type + 32, // format + 1, // data_len + &net_wm_window_type_normal // data + ); + + xcb_map_window(conn.data(), transientWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + Client *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->windowId(), transientWid); + QCOMPARE(transient->group(), leader->group()); + QVERIFY(transient->isTransient()); + QVERIFY(transient->groupTransient()); + QVERIFY(!transient->isDialog()); // See above why + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1, member2, transient})); + + // Create a Wayland client that is not a member of the window group. + KWayland::Client::Surface *anotherSurface = + Test::createSurface(Test::waylandCompositor()); + QVERIFY(anotherSurface); + KWayland::Client::ShellSurface *anotherShellSurface = + Test::createShellSurface(anotherSurface, anotherSurface); + QVERIFY(anotherShellSurface); + ShellClient *anotherClient = Test::renderAndWaitForShown(anotherSurface, QSize(128, 128), Qt::green); + QVERIFY(anotherClient); + QVERIFY(anotherClient->isActive()); + QVERIFY(!anotherClient->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1, member2, transient, anotherClient})); + + // Raise the transient to bring the window group to initial test position. + workspace()->activateClient(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{anotherClient, leader, member1, member2, transient})); + + // Lower the transient, the window group should be lowered too. + workspace()->lowerClient(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1, member2, transient, anotherClient})); +} + void StackingOrderTest::testRaiseGroupTransient() { const QRect geometry = QRect(0, 0, 128, 128); @@ -814,6 +1003,90 @@ QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1, member2, transient})); } +void StackingOrderTest::testDontLowerWindowGroup() +{ + // This test verifies that a window group won't be lowered if a transient for some + // window in the window group is lowered. + + const QRect geometry = QRect(0, 0, 128, 128); + + QScopedPointer conn( + xcb_connect(nullptr, nullptr)); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + + // Create the group leader. + xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry); + xcb_map_window(conn.data(), leaderWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + Client *leader = windowCreatedSpy.first().first().value(); + QVERIFY(leader); + QVERIFY(leader->isActive()); + QCOMPARE(leader->windowId(), leaderWid); + QVERIFY(!leader->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader})); + + // Create another group member. + windowCreatedSpy.clear(); + xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member1Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + Client *member1 = windowCreatedSpy.first().first().value(); + QVERIFY(member1); + QVERIFY(member1->isActive()); + QCOMPARE(member1->windowId(), member1Wid); + QCOMPARE(member1->group(), leader->group()); + QVERIFY(!member1->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1})); + + // Create yet another group member. + windowCreatedSpy.clear(); + xcb_window_t member2Wid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_map_window(conn.data(), member2Wid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + Client *member2 = windowCreatedSpy.first().first().value(); + QVERIFY(member2); + QVERIFY(member2->isActive()); + QCOMPARE(member2->windowId(), member2Wid); + QCOMPARE(member2->group(), leader->group()); + QVERIFY(!member2->isTransient()); + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1, member2})); + + // Create the transient. + windowCreatedSpy.clear(); + xcb_window_t transientWid = createGroupWindow(conn.data(), geometry, leaderWid); + xcb_icccm_set_wm_transient_for(conn.data(), transientWid, member1Wid); + xcb_map_window(conn.data(), transientWid); + xcb_flush(conn.data()); + + QVERIFY(windowCreatedSpy.wait()); + Client *transient = windowCreatedSpy.first().first().value(); + QVERIFY(transient); + QVERIFY(transient->isActive()); + QCOMPARE(transient->windowId(), transientWid); + QCOMPARE(transient->group(), leader->group()); + QVERIFY(transient->isTransient()); + QVERIFY(!transient->groupTransient()); + QCOMPARE(transient->transientFor(), member1); + + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{leader, member1, member2, transient})); + + // Lower the transient, only it and its parent have to be lowered. + workspace()->lowerClient(transient); + QTRY_VERIFY(transient->isActive()); + QCOMPARE(workspace()->stackingOrder(), (ToplevelList{member1, transient, leader, member2})); +} + void StackingOrderTest::testDontRaiseWindowGroup() { // This test verifies that a window group won't be raised if a transient for some diff --git a/layers.cpp b/layers.cpp --- a/layers.cpp +++ b/layers.cpp @@ -311,16 +311,9 @@ unconstrained_stacking_order.removeAll(c); unconstrained_stacking_order.prepend(c); if (!nogroup && c->isTransient()) { - // lower also all windows in the group, in their reversed stacking order - ClientList wins; - if (Client *client = dynamic_cast(c)) { - wins = ensureStackingOrder(client->group()->members()); - } - for (int i = wins.size() - 1; - i >= 0; - --i) { - if (wins[ i ] != c) - lowerClient(wins[ i ], true); + const auto mainClients = ensureStackingOrder(c->allMainClients()); + for (int i = mainClients.count() - 1; i >= 0; --i) { + lowerClient(mainClients[i], true); } }