/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2016 Martin Gräßlin SPDX-FileCopyrightText: 2019 David Edmundson SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "cursor.h" #include "decorations/decorationbridge.h" #include "decorations/settings.h" #include "deleted.h" #include "effects.h" #include "output.h" #include "platform.h" #include "screens.h" #include "virtualdesktops.h" #include "wayland/clientconnection.h" #include "wayland/display.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // system #include #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellwindow-0"); class TestXdgShellWindow : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMapUnmap(); void testDesktopPresenceChanged(); void testWindowOutputs(); void testMinimizeActiveWindow(); void testFullscreen_data(); void testFullscreen(); void testUserCanSetFullscreen(); void testMaximizeHorizontal(); void testMaximizeVertical(); void testMaximizeFull(); void testMaximizedToFullscreen_data(); void testMaximizedToFullscreen(); void testFullscreenMultipleOutputs(); void testHidden(); void testDesktopFileName(); void testCaptionSimplified(); void testCaptionMultipleWindows(); void testUnresponsiveWindow_data(); void testUnresponsiveWindow(); void testAppMenu(); void testSendClientWithTransientToDesktop(); void testMinimizeWindowWithTransients(); void testXdgDecoration_data(); void testXdgDecoration(); void testXdgNeverCommitted(); void testXdgInitialState(); void testXdgInitiallyMaximised(); void testXdgInitiallyFullscreen(); void testXdgInitiallyMinimized(); void testXdgWindowGeometryIsntSet(); void testXdgWindowGeometryAttachBuffer(); void testXdgWindowGeometryAttachSubSurface(); void testXdgWindowGeometryInteractiveResize(); void testXdgWindowGeometryFullScreen(); void testXdgWindowGeometryMaximize(); void testXdgWindowReactive(); void testXdgWindowRepositioning(); void testPointerInputTransform(); void testReentrantSetFrameGeometry(); void testDoubleMaximize(); void testDoubleFullscreenSeparatedByCommit(); void testMaximizeAndChangeDecorationModeAfterInitialCommit(); void testFullScreenAndChangeDecorationModeAfterInitialCommit(); void testChangeDecorationModeAfterInitialCommit(); }; void TestXdgShellWindow::testXdgWindowReactive() { QScopedPointer positioner(Test::createXdgPositioner()); positioner->set_size(10, 10); positioner->set_anchor_rect(10, 10, 10, 10); positioner->set_reactive(); QScopedPointer rootSurface(Test::createSurface()); QScopedPointer root(Test::createXdgToplevelSurface(rootSurface.data())); auto rootClient = Test::renderAndWaitForShown(rootSurface.data(), QSize(100, 100), Qt::cyan); QVERIFY(rootClient); QScopedPointer childSurface(Test::createSurface()); QScopedPointer popup(Test::createXdgPopupSurface(childSurface.data(), root->xdgSurface(), positioner.data())); auto childClient = Test::renderAndWaitForShown(childSurface.data(), QSize(10, 10), Qt::cyan); QVERIFY(childClient); QSignalSpy popupConfigureRequested(popup.data(), &Test::XdgPopup::configureRequested); QVERIFY(popupConfigureRequested.isValid()); rootClient->move(rootClient->pos() + QPoint(20, 20)); QVERIFY(popupConfigureRequested.wait()); QCOMPARE(popupConfigureRequested.count(), 1); } void TestXdgShellWindow::testXdgWindowRepositioning() { QScopedPointer positioner(Test::createXdgPositioner()); positioner->set_size(10, 10); positioner->set_anchor_rect(10, 10, 10, 10); QScopedPointer otherPositioner(Test::createXdgPositioner()); otherPositioner->set_size(50, 50); otherPositioner->set_anchor_rect(10, 10, 10, 10); QScopedPointer rootSurface(Test::createSurface()); QScopedPointer root(Test::createXdgToplevelSurface(rootSurface.data())); auto rootClient = Test::renderAndWaitForShown(rootSurface.data(), QSize(100, 100), Qt::cyan); QVERIFY(rootClient); QScopedPointer childSurface(Test::createSurface()); QScopedPointer popup(Test::createXdgPopupSurface(childSurface.data(), root->xdgSurface(), positioner.data())); auto childClient = Test::renderAndWaitForShown(childSurface.data(), QSize(10, 10), Qt::cyan); QVERIFY(childClient); QSignalSpy reconfigureSpy(popup.data(), &Test::XdgPopup::configureRequested); QVERIFY(reconfigureSpy.isValid()); popup->reposition(otherPositioner->object(), 500000); QVERIFY(reconfigureSpy.wait()); QCOMPARE(reconfigureSpy.count(), 1); } void TestXdgShellWindow::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); QVERIFY(applicationStartedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); kwinApp()->start(); QVERIFY(applicationStartedSpy.wait()); const auto outputs = kwinApp()->platform()->enabledOutputs(); QCOMPARE(outputs.count(), 2); QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024)); Test::initWaylandWorkspace(); } void TestXdgShellWindow::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::XdgDecorationV1 | Test::AdditionalWaylandInterface::AppMenu)); QVERIFY(Test::waitForWaylandPointer()); workspace()->setActiveOutput(QPoint(640, 512)); // put mouse in the middle of screen one KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); } void TestXdgShellWindow::cleanup() { Test::destroyWaylandConnection(); } void TestXdgShellWindow::testMapUnmap() { // This test verifies that the compositor destroys XdgToplevelWindow when the // associated xdg_toplevel surface is unmapped. // Create a wl_surface and an xdg_toplevel, but don't commit them yet! QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); QVERIFY(windowAddedSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); // Tell the compositor that we want to map the surface. surface->commit(KWayland::Client::Surface::CommitFlag::None); // The compositor will respond with a configure event. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); // Now we can attach a buffer with actual data to the surface. Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(windowAddedSpy.wait()); QCOMPARE(windowAddedSpy.count(), 1); Window *client = windowAddedSpy.last().first().value(); QVERIFY(client); QCOMPARE(client->readyForPainting(), true); // When the client becomes active, the compositor will send another configure event. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); // Unmap the xdg_toplevel surface by committing a null buffer. surface->attachBuffer(Buffer::Ptr()); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(Test::waitForWindowDestroyed(client)); // Tell the compositor that we want to re-map the xdg_toplevel surface. surface->commit(KWayland::Client::Surface::CommitFlag::None); // The compositor will respond with a configure event. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); // Now we can attach a buffer with actual data to the surface. Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(windowAddedSpy.wait()); QCOMPARE(windowAddedSpy.count(), 2); client = windowAddedSpy.last().first().value(); QVERIFY(client); QCOMPARE(client->readyForPainting(), true); // The compositor will respond with a configure event. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); // Destroy the test client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testDesktopPresenceChanged() { // this test verifies that the desktop presence changed signals are properly emitted QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->desktop(), 1); effects->setNumberOfDesktops(4); QSignalSpy desktopPresenceChangedClientSpy(c, &Window::desktopPresenceChanged); QVERIFY(desktopPresenceChangedClientSpy.isValid()); QSignalSpy desktopPresenceChangedWorkspaceSpy(workspace(), &Workspace::desktopPresenceChanged); QVERIFY(desktopPresenceChangedWorkspaceSpy.isValid()); QSignalSpy desktopPresenceChangedEffectsSpy(effects, &EffectsHandler::desktopPresenceChanged); QVERIFY(desktopPresenceChangedEffectsSpy.isValid()); // let's change the desktop workspace()->sendWindowToDesktop(c, 2, false); QCOMPARE(c->desktop(), 2); QCOMPARE(desktopPresenceChangedClientSpy.count(), 1); QCOMPARE(desktopPresenceChangedWorkspaceSpy.count(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.count(), 1); // verify the arguments QCOMPARE(desktopPresenceChangedClientSpy.first().at(0).value(), c); QCOMPARE(desktopPresenceChangedClientSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedWorkspaceSpy.first().at(0).value(), c); QCOMPARE(desktopPresenceChangedWorkspaceSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(0).value(), c->effectWindow()); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(2).toInt(), 2); } void TestXdgShellWindow::testWindowOutputs() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); auto size = QSize(200, 200); QSignalSpy outputEnteredSpy(surface.data(), &KWayland::Client::Surface::outputEntered); QSignalSpy outputLeftSpy(surface.data(), &KWayland::Client::Surface::outputLeft); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); // move to be in the first screen c->moveResize(QRect(QPoint(100, 100), size)); // we don't don't know where the compositor first placed this window, // this might fire, it might not outputEnteredSpy.wait(5); outputEnteredSpy.clear(); QCOMPARE(surface->outputs().count(), 1); QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(0, 0)); // move to overlapping both first and second screen c->moveResize(QRect(QPoint(1250, 100), size)); QVERIFY(outputEnteredSpy.wait()); QCOMPARE(outputEnteredSpy.count(), 1); QCOMPARE(outputLeftSpy.count(), 0); QCOMPARE(surface->outputs().count(), 2); QVERIFY(surface->outputs()[0] != surface->outputs()[1]); // move entirely into second screen c->moveResize(QRect(QPoint(1400, 100), size)); QVERIFY(outputLeftSpy.wait()); QCOMPARE(outputEnteredSpy.count(), 1); QCOMPARE(outputLeftSpy.count(), 1); QCOMPARE(surface->outputs().count(), 1); QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(1280, 0)); } void TestXdgShellWindow::testMinimizeActiveWindow() { // this test verifies that when minimizing the active window it gets deactivated QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(workspace()->activeWindow(), c); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); QVERIFY(c->isShown()); workspace()->slotWindowMinimize(); QVERIFY(!c->isShown()); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); QVERIFY(!c->isActive()); QVERIFY(!workspace()->activeWindow()); QVERIFY(c->isMinimized()); // unminimize again c->unminimize(); QVERIFY(!c->isMinimized()); QVERIFY(c->isActive()); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); QVERIFY(c->isShown()); QCOMPARE(workspace()->activeWindow(), c); } void TestXdgShellWindow::testFullscreen_data() { QTest::addColumn("decoMode"); QTest::newRow("client-side deco") << Test::XdgToplevelDecorationV1::mode_client_side; QTest::newRow("server-side deco") << Test::XdgToplevelDecorationV1::mode_server_side; } void TestXdgShellWindow::testFullscreen() { // this test verifies that a window can be properly fullscreened Test::XdgToplevel::States states; QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer decoration(Test::createXdgToplevelDecorationV1(shellSurface.data())); QSignalSpy decorationConfigureRequestedSpy(decoration.data(), &Test::XdgToplevelDecorationV1::configureRequested); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); // Initialize the xdg-toplevel surface. QFETCH(Test::XdgToplevelDecorationV1::mode, decoMode); decoration->set_mode(decoMode); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->layer(), NormalLayer); QVERIFY(!client->isFullScreen()); QCOMPARE(client->clientSize(), QSize(100, 50)); QCOMPARE(client->isDecorated(), decoMode == Test::XdgToplevelDecorationV1::mode_server_side); QCOMPARE(client->clientSizeToFrameSize(client->clientSize()), client->size()); QSignalSpy fullScreenChangedSpy(client, &Window::fullScreenChanged); QVERIFY(fullScreenChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); // Wait for the compositor to send a configure event with the Activated state. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states & Test::XdgToplevel::State::Activated); // Ask the compositor to show the window in full screen mode. shellSurface->set_fullscreen(nullptr); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states & Test::XdgToplevel::State::Fullscreen); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), client->output()->geometry().size()); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); QVERIFY(fullScreenChangedSpy.wait()); QCOMPARE(fullScreenChangedSpy.count(), 1); QVERIFY(client->isFullScreen()); QVERIFY(!client->isDecorated()); QCOMPARE(client->layer(), ActiveLayer); QCOMPARE(client->frameGeometry(), QRect(QPoint(0, 0), client->output()->geometry().size())); // Ask the compositor to show the window in normal mode. shellSurface->unset_fullscreen(); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!(states & Test::XdgToplevel::State::Fullscreen)); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(100, 50)); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::blue); QVERIFY(fullScreenChangedSpy.wait()); QCOMPARE(fullScreenChangedSpy.count(), 2); QCOMPARE(client->clientSize(), QSize(100, 50)); QVERIFY(!client->isFullScreen()); QCOMPARE(client->isDecorated(), decoMode == Test::XdgToplevelDecorationV1::mode_server_side); QCOMPARE(client->layer(), NormalLayer); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testUserCanSetFullscreen() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QVERIFY(!c->isFullScreen()); QVERIFY(c->userCanSetFullScreen()); } void TestXdgShellWindow::testMaximizedToFullscreen_data() { QTest::addColumn("decoMode"); QTest::newRow("client-side deco") << Test::XdgToplevelDecorationV1::mode_client_side; QTest::newRow("server-side deco") << Test::XdgToplevelDecorationV1::mode_server_side; } void TestXdgShellWindow::testMaximizedToFullscreen() { // this test verifies that a window can be properly fullscreened after maximizing Test::XdgToplevel::States states; QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer decoration(Test::createXdgToplevelDecorationV1(shellSurface.data())); QSignalSpy decorationConfigureRequestedSpy(decoration.data(), &Test::XdgToplevelDecorationV1::configureRequested); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); // Initialize the xdg-toplevel surface. QFETCH(Test::XdgToplevelDecorationV1::mode, decoMode); decoration->set_mode(decoMode); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isFullScreen()); QCOMPARE(client->clientSize(), QSize(100, 50)); QCOMPARE(client->isDecorated(), decoMode == Test::XdgToplevelDecorationV1::mode_server_side); QSignalSpy fullscreenChangedSpy(client, &Window::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); // Wait for the compositor to send a configure event with the Activated state. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states & Test::XdgToplevel::State::Activated); // Ask the compositor to maximize the window. shellSurface->set_maximized(); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states & Test::XdgToplevel::State::Maximized); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->maximizeMode(), MaximizeFull); // Ask the compositor to show the window in full screen mode. shellSurface->set_fullscreen(nullptr); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), client->output()->geometry().size()); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states & Test::XdgToplevel::State::Maximized); QVERIFY(states & Test::XdgToplevel::State::Fullscreen); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); QVERIFY(fullscreenChangedSpy.wait()); QCOMPARE(fullscreenChangedSpy.count(), 1); QCOMPARE(client->maximizeMode(), MaximizeFull); QVERIFY(client->isFullScreen()); QVERIFY(!client->isDecorated()); // Switch back to normal mode. shellSurface->unset_fullscreen(); shellSurface->unset_maximized(); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 5); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(100, 50)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!(states & Test::XdgToplevel::State::Maximized)); QVERIFY(!(states & Test::XdgToplevel::State::Fullscreen)); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QVERIFY(!client->isFullScreen()); QCOMPARE(client->isDecorated(), decoMode == Test::XdgToplevelDecorationV1::mode_server_side); QCOMPARE(client->maximizeMode(), MaximizeRestore); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testFullscreenMultipleOutputs() { // this test verifies that kwin will place fullscreen windows in the outputs its instructed to const auto outputs = kwinApp()->platform()->enabledOutputs(); for (int i = 0; i < outputs.count(); ++i) { Test::XdgToplevel::States states; QSharedPointer surface(Test::createSurface()); QVERIFY(surface); QSharedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); QVERIFY(shellSurface); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isFullScreen()); QCOMPARE(client->clientSize(), QSize(100, 50)); QVERIFY(!client->isDecorated()); QSignalSpy fullscreenChangedSpy(client, &Window::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); // Wait for the compositor to send a configure event with the Activated state. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); // Ask the compositor to show the window in full screen mode. shellSurface->set_fullscreen(*Test::waylandOutputs()[i]); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), outputs[i]->geometry().size()); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), toplevelConfigureRequestedSpy.last().at(0).value(), Qt::red); QVERIFY(!fullscreenChangedSpy.isEmpty() || fullscreenChangedSpy.wait()); QCOMPARE(fullscreenChangedSpy.count(), 1); QVERIFY(!frameGeometryChangedSpy.isEmpty() || frameGeometryChangedSpy.wait()); QVERIFY(client->isFullScreen()); QCOMPARE(client->frameGeometry(), screens()->geometry(i)); } } void TestXdgShellWindow::testHidden() { // this test verifies that when hiding window it doesn't get shown QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(workspace()->activeWindow(), c); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); QVERIFY(c->isShown()); c->hideClient(); QVERIFY(!c->isShown()); QVERIFY(!c->isActive()); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); // unhide again c->showClient(); QVERIFY(c->isShown()); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); // QCOMPARE(workspace()->activeClient(), c); } void TestXdgShellWindow::testDesktopFileName() { QIcon::setThemeName(QStringLiteral("breeze")); // this test verifies that desktop file name is passed correctly to the window QScopedPointer surface(Test::createSurface()); // only xdg-shell as ShellSurface misses the setter QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); shellSurface->set_app_id(QStringLiteral("org.kde.foo")); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.foo")); QCOMPARE(c->resourceClass(), QByteArrayLiteral("org.kde.foo")); QVERIFY(c->resourceName().startsWith("testXdgShellWindow")); // the desktop file does not exist, so icon should be generic Wayland QCOMPARE(c->icon().name(), QStringLiteral("wayland")); QSignalSpy desktopFileNameChangedSpy(c, &Window::desktopFileNameChanged); QVERIFY(desktopFileNameChangedSpy.isValid()); QSignalSpy iconChangedSpy(c, &Window::iconChanged); QVERIFY(iconChangedSpy.isValid()); shellSurface->set_app_id(QStringLiteral("org.kde.bar")); QVERIFY(desktopFileNameChangedSpy.wait()); QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.bar")); QCOMPARE(c->resourceClass(), QByteArrayLiteral("org.kde.bar")); QVERIFY(c->resourceName().startsWith("testXdgShellWindow")); // icon should still be wayland QCOMPARE(c->icon().name(), QStringLiteral("wayland")); QVERIFY(iconChangedSpy.isEmpty()); const QString dfPath = QFINDTESTDATA("data/example.desktop"); shellSurface->set_app_id(dfPath.toUtf8()); QVERIFY(desktopFileNameChangedSpy.wait()); QCOMPARE(iconChangedSpy.count(), 1); QCOMPARE(QString::fromUtf8(c->desktopFileName()), dfPath); QCOMPARE(c->icon().name(), QStringLiteral("kwin")); } void TestXdgShellWindow::testCaptionSimplified() { // this test verifies that caption is properly trimmed // see BUG 323798 comment #12 QScopedPointer surface(Test::createSurface()); // only done for xdg-shell as ShellSurface misses the setter QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); const QString origTitle = QString::fromUtf8(QByteArrayLiteral("Was tun, wenn Schüler Autismus haben?\342\200\250\342\200\250\342\200\250 – Marlies Hübner - Mozilla Firefox")); shellSurface->set_title(origTitle); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->caption() != origTitle); QCOMPARE(c->caption(), origTitle.simplified()); } void TestXdgShellWindow::testCaptionMultipleWindows() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); shellSurface->set_title(QStringLiteral("foo")); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->caption(), QStringLiteral("foo")); QCOMPARE(c->captionNormal(), QStringLiteral("foo")); QCOMPARE(c->captionSuffix(), QString()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgToplevelSurface(surface2.data())); shellSurface2->set_title(QStringLiteral("foo")); auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(c2); QCOMPARE(c2->caption(), QStringLiteral("foo <2>")); QCOMPARE(c2->captionNormal(), QStringLiteral("foo")); QCOMPARE(c2->captionSuffix(), QStringLiteral(" <2>")); QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgToplevelSurface(surface3.data())); shellSurface3->set_title(QStringLiteral("foo")); auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(c3); QCOMPARE(c3->caption(), QStringLiteral("foo <3>")); QCOMPARE(c3->captionNormal(), QStringLiteral("foo")); QCOMPARE(c3->captionSuffix(), QStringLiteral(" <3>")); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgToplevelSurface(surface4.data())); shellSurface4->set_title(QStringLiteral("bar")); auto c4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(c4); QCOMPARE(c4->caption(), QStringLiteral("bar")); QCOMPARE(c4->captionNormal(), QStringLiteral("bar")); QCOMPARE(c4->captionSuffix(), QString()); QSignalSpy captionChangedSpy(c4, &Window::captionChanged); QVERIFY(captionChangedSpy.isValid()); shellSurface4->set_title(QStringLiteral("foo")); QVERIFY(captionChangedSpy.wait()); QCOMPARE(captionChangedSpy.count(), 1); QCOMPARE(c4->caption(), QStringLiteral("foo <4>")); QCOMPARE(c4->captionNormal(), QStringLiteral("foo")); QCOMPARE(c4->captionSuffix(), QStringLiteral(" <4>")); } void TestXdgShellWindow::testUnresponsiveWindow_data() { QTest::addColumn("shellInterface"); // see env selection in qwaylandintegration.cpp QTest::addColumn("socketMode"); QTest::newRow("xdg display") << "xdg-shell" << false; QTest::newRow("xdg socket") << "xdg-shell" << true; } void TestXdgShellWindow::testUnresponsiveWindow() { // this test verifies that killWindow properly terminates a process // for this an external binary is launched const QString kill = QFINDTESTDATA(QStringLiteral("kill")); QVERIFY(!kill.isEmpty()); QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); QVERIFY(windowAddedSpy.isValid()); QScopedPointer process(new QProcess); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QFETCH(QString, shellInterface); QFETCH(bool, socketMode); env.insert("QT_WAYLAND_SHELL_INTEGRATION", shellInterface); if (socketMode) { int sx[2]; QVERIFY(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) >= 0); waylandServer()->display()->createClient(sx[0]); int socket = dup(sx[1]); QVERIFY(socket != -1); env.insert(QStringLiteral("WAYLAND_SOCKET"), QByteArray::number(socket)); env.remove("WAYLAND_DISPLAY"); } else { env.insert("WAYLAND_DISPLAY", s_socketName); } process->setProcessEnvironment(env); process->setProcessChannelMode(QProcess::ForwardedChannels); process->setProgram(kill); QSignalSpy processStartedSpy{process.data(), &QProcess::started}; QVERIFY(processStartedSpy.isValid()); process->start(); QVERIFY(processStartedSpy.wait()); Window *killClient = nullptr; if (windowAddedSpy.isEmpty()) { QVERIFY(windowAddedSpy.wait()); } ::kill(process->processId(), SIGUSR1); // send a signal to freeze the process killClient = windowAddedSpy.first().first().value(); QVERIFY(killClient); QSignalSpy unresponsiveSpy(killClient, &Window::unresponsiveChanged); QSignalSpy killedSpy(process.data(), static_cast(&QProcess::finished)); QSignalSpy deletedSpy(killClient, &QObject::destroyed); qint64 startTime = QDateTime::currentMSecsSinceEpoch(); // wait for the process to be frozen QTest::qWait(10); // pretend the user clicked the close button killClient->closeWindow(); // client should not yet be marked unresponsive nor killed QVERIFY(!killClient->unresponsive()); QVERIFY(killedSpy.isEmpty()); QVERIFY(unresponsiveSpy.wait()); // client should be marked unresponsive but not killed auto elapsed1 = QDateTime::currentMSecsSinceEpoch() - startTime; QVERIFY(elapsed1 > 900 && elapsed1 < 1200); // ping timer is 1s, but coarse timers on a test across two processes means we need a fuzzy compare QVERIFY(killClient->unresponsive()); QVERIFY(killedSpy.isEmpty()); QVERIFY(deletedSpy.wait()); if (!socketMode) { // process was killed - because we're across process this could happen in either order QVERIFY(killedSpy.count() || killedSpy.wait()); } auto elapsed2 = QDateTime::currentMSecsSinceEpoch() - startTime; QVERIFY(elapsed2 > 1800); // second ping comes in a second later } void TestXdgShellWindow::testAppMenu() { // register a faux appmenu client QVERIFY(QDBusConnection::sessionBus().registerService("org.kde.kappmenu")); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QScopedPointer menu(Test::waylandAppMenuManager()->create(surface.data())); QSignalSpy spy(c, &Window::hasApplicationMenuChanged); menu->setAddress("service.name", "object/path"); spy.wait(); QCOMPARE(c->hasApplicationMenu(), true); QCOMPARE(c->applicationMenuServiceName(), QString("service.name")); QCOMPARE(c->applicationMenuObjectPath(), QString("object/path")); QVERIFY(QDBusConnection::sessionBus().unregisterService("org.kde.kappmenu")); } void TestXdgShellWindow::testSendClientWithTransientToDesktop() { // this test verifies that when sending a client to a desktop all transients are also send to that desktop VirtualDesktopManager *vds = VirtualDesktopManager::self(); vds->setCount(2); const QVector desktops = vds->desktops(); QScopedPointer surface{Test::createSurface()}; QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); // let's create a transient window QScopedPointer transientSurface{Test::createSurface()}; QScopedPointer transientShellSurface(Test::createXdgToplevelSurface(transientSurface.data())); transientShellSurface->set_parent(shellSurface->object()); auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(100, 50), Qt::blue); QVERIFY(transient); QCOMPARE(workspace()->activeWindow(), transient); QCOMPARE(transient->transientFor(), c); QVERIFY(c->transients().contains(transient)); // initially, the parent and the transient are on the first virtual desktop QCOMPARE(c->desktops(), QVector{desktops[0]}); QVERIFY(!c->isOnAllDesktops()); QCOMPARE(transient->desktops(), QVector{desktops[0]}); QVERIFY(!transient->isOnAllDesktops()); // send the transient to the second virtual desktop workspace()->slotWindowToDesktop(desktops[1]); QCOMPARE(c->desktops(), QVector{desktops[0]}); QCOMPARE(transient->desktops(), QVector{desktops[1]}); // activate c workspace()->activateWindow(c); QCOMPARE(workspace()->activeWindow(), c); QVERIFY(c->isActive()); // and send it to the desktop it's already on QCOMPARE(c->desktops(), QVector{desktops[0]}); QCOMPARE(transient->desktops(), QVector{desktops[1]}); workspace()->slotWindowToDesktop(desktops[0]); // which should move the transient back to the desktop QCOMPARE(c->desktops(), QVector{desktops[0]}); QCOMPARE(transient->desktops(), QVector{desktops[0]}); } void TestXdgShellWindow::testMinimizeWindowWithTransients() { // this test verifies that when minimizing/unminimizing a window all its // transients will be minimized/unminimized as well // create the main window QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(!c->isMinimized()); // create a transient window QScopedPointer transientSurface(Test::createSurface()); QScopedPointer transientShellSurface(Test::createXdgToplevelSurface(transientSurface.data())); transientShellSurface->set_parent(shellSurface->object()); auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(100, 50), Qt::red); QVERIFY(transient); QVERIFY(!transient->isMinimized()); QCOMPARE(transient->transientFor(), c); QVERIFY(c->hasTransient(transient, false)); // minimize the main window, the transient should be minimized as well c->minimize(); QVERIFY(c->isMinimized()); QVERIFY(transient->isMinimized()); // unminimize the main window, the transient should be unminimized as well c->unminimize(); QVERIFY(!c->isMinimized()); QVERIFY(!transient->isMinimized()); } void TestXdgShellWindow::testXdgDecoration_data() { QTest::addColumn("requestedMode"); QTest::addColumn("expectedMode"); QTest::newRow("client side requested") << Test::XdgToplevelDecorationV1::mode_client_side << Test::XdgToplevelDecorationV1::mode_client_side; QTest::newRow("server side requested") << Test::XdgToplevelDecorationV1::mode_server_side << Test::XdgToplevelDecorationV1::mode_server_side; } void TestXdgShellWindow::testXdgDecoration() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); QScopedPointer deco(Test::createXdgToplevelDecorationV1(shellSurface.data())); QSignalSpy decorationConfigureRequestedSpy(deco.data(), &Test::XdgToplevelDecorationV1::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); QFETCH(Test::XdgToplevelDecorationV1::mode, requestedMode); QFETCH(Test::XdgToplevelDecorationV1::mode, expectedMode); // request a mode deco->set_mode(requestedMode); // kwin will send a configure QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(decorationConfigureRequestedSpy.count(), 1); QCOMPARE(decorationConfigureRequestedSpy.last()[0].value(), expectedMode); QVERIFY(decorationConfigureRequestedSpy.count() > 0); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last()[0].toInt()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QCOMPARE(c->isDecorated(), expectedMode == Test::XdgToplevelDecorationV1::mode_server_side); } void TestXdgShellWindow::testXdgNeverCommitted() { // check we don't crash if we create a shell object but delete the XdgShellClient before committing it QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); } void TestXdgShellWindow::testXdgInitialState() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); surfaceConfigureRequestedSpy.wait(); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); const auto size = toplevelConfigureRequestedSpy.first()[0].value(); QCOMPARE(size, QSize(0, 0)); // client should chose it's preferred size shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::blue); QCOMPARE(c->size(), QSize(200, 100)); } void TestXdgShellWindow::testXdgInitiallyMaximised() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); shellSurface->set_maximized(); surface->commit(KWayland::Client::Surface::CommitFlag::None); surfaceConfigureRequestedSpy.wait(); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); const auto size = toplevelConfigureRequestedSpy.first()[0].value(); const auto state = toplevelConfigureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(1280, 1024)); QVERIFY(state & Test::XdgToplevel::State::Maximized); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); QCOMPARE(c->maximizeMode(), MaximizeFull); QCOMPARE(c->size(), QSize(1280, 1024)); } void TestXdgShellWindow::testXdgInitiallyFullscreen() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); shellSurface->set_fullscreen(nullptr); surface->commit(KWayland::Client::Surface::CommitFlag::None); surfaceConfigureRequestedSpy.wait(); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); const auto size = toplevelConfigureRequestedSpy.first()[0].value(); const auto state = toplevelConfigureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(1280, 1024)); QVERIFY(state & Test::XdgToplevel::State::Fullscreen); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); QCOMPARE(c->isFullScreen(), true); QCOMPARE(c->size(), QSize(1280, 1024)); } void TestXdgShellWindow::testXdgInitiallyMinimized() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); shellSurface->set_minimized(); surface->commit(KWayland::Client::Surface::CommitFlag::None); surfaceConfigureRequestedSpy.wait(); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); const auto size = toplevelConfigureRequestedSpy.first()[0].value(); const auto state = toplevelConfigureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(0, 0)); QCOMPARE(state, 0); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.first()[0].toUInt()); QEXPECT_FAIL("", "Client created in a minimised state is not exposed to kwin bug 404838", Abort); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue, QImage::Format_ARGB32, 10); QVERIFY(c); QVERIFY(c->isMinimized()); } void TestXdgShellWindow::testXdgWindowGeometryIsntSet() { // This test verifies that the effective window geometry corresponds to the // bounding rectangle of the main surface and its sub-surfaces if no window // geometry is set by the client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); QScopedPointer childSurface(Test::createSurface()); QScopedPointer subSurface(Test::createSubSurface(childSurface.data(), surface.data())); QVERIFY(subSurface); subSurface->setPosition(QPoint(-20, -10)); Test::render(childSurface.data(), QSize(100, 50), Qt::blue); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(120, 60)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition + QPoint(20, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); } void TestXdgShellWindow::testXdgWindowGeometryAttachBuffer() { // This test verifies that the effective window geometry remains the same when // a new buffer is attached and xdg_surface.set_window_geometry is not called // again. Notice that the window geometry must remain the same even if the new // buffer is smaller. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 2); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(90, 40)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); shellSurface->xdgSurface()->set_window_geometry(0, 0, 100, 50); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 3); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testXdgWindowGeometryAttachSubSurface() { // This test verifies that the effective window geometry remains the same // when a new sub-surface is added and xdg_surface.set_window_geometry is // not called again. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QScopedPointer childSurface(Test::createSurface()); QScopedPointer subSurface(Test::createSubSurface(childSurface.data(), surface.data())); QVERIFY(subSurface); subSurface->setPosition(QPoint(-20, -20)); Test::render(childSurface.data(), QSize(100, 50), Qt::blue); surface->commit(KWayland::Client::Surface::CommitFlag::None); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); shellSurface->xdgSurface()->set_window_geometry(-15, -15, 50, 40); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(50, 40)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(-15, -15)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); } void TestXdgShellWindow::testXdgWindowGeometryInteractiveResize() { // This test verifies that correct window geometry is provided along each // configure event when an xdg-shell is being interactively resized. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); QVERIFY(surfaceConfigureRequestedSpy.isValid()); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QSignalSpy clientStartMoveResizedSpy(client, &Window::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &Window::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &Window::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); // Start interactively resizing the client. QCOMPARE(workspace()->moveResizeWindow(), nullptr); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeWindow(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); Test::XdgToplevel::States states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); // Go right. QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Right); client->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(188, 80)); shellSurface->xdgSurface()->set_window_geometry(10, 10, 188, 80); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(208, 100), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->bufferGeometry().size(), QSize(208, 100)); QCOMPARE(client->frameGeometry().size(), QSize(188, 80)); // Go down. cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Down); client->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(0, 8)); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing)); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(188, 88)); shellSurface->xdgSurface()->set_window_geometry(10, 10, 188, 88); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(208, 108), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); QCOMPARE(client->bufferGeometry().size(), QSize(208, 108)); QCOMPARE(client->frameGeometry().size(), QSize(188, 88)); // Finish resizing the client. client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeWindow(), nullptr); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 5); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testXdgWindowGeometryFullScreen() { // This test verifies that an xdg-shell receives correct window geometry when // its fullscreen state gets changed. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); QVERIFY(surfaceConfigureRequestedSpy.isValid()); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); workspace()->slotWindowFullScreen(); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); Test::XdgToplevel::States states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Fullscreen)); shellSurface->xdgSurface()->set_window_geometry(0, 0, 1280, 1024); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(1280, 1024)); QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); workspace()->slotWindowFullScreen(); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Fullscreen)); shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(200, 100), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testXdgWindowGeometryMaximize() { // This test verifies that an xdg-shell receives correct window geometry when // its maximized state gets changed. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); QVERIFY(surfaceConfigureRequestedSpy.isValid()); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); workspace()->slotWindowMaximize(); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); Test::XdgToplevel::States states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); shellSurface->xdgSurface()->set_window_geometry(0, 0, 1280, 1024); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(1280, 1024)); QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); workspace()->slotWindowMaximize(); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); shellSurface->xdgSurface()->set_window_geometry(10, 10, 180, 80); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(200, 100), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testPointerInputTransform() { // This test verifies that XdgToplevelWindow provides correct input transform matrix. // The input transform matrix is used by seat to map pointer events from the global // screen coordinates to the surface-local coordinates. // Get a wl_pointer object on the client side. QScopedPointer pointer(Test::waylandSeat()->createPointer()); QVERIFY(pointer); QVERIFY(pointer->isValid()); QSignalSpy pointerEnteredSpy(pointer.data(), &KWayland::Client::Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); QSignalSpy pointerMotionSpy(pointer.data(), &KWayland::Client::Pointer::motion); QVERIFY(pointerMotionSpy.isValid()); // Create an xdg_toplevel surface and wait for the compositor to catch up. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); // Enter the surface. quint32 timestamp = 0; Test::pointerMotion(client->pos(), timestamp++); QVERIFY(pointerEnteredSpy.wait()); // Move the pointer to (10, 5) relative to the upper left frame corner, which is located // at (0, 0) in the surface-local coordinates. Test::pointerMotion(client->pos() + QPoint(10, 5), timestamp++); QVERIFY(pointerMotionSpy.wait()); QCOMPARE(pointerMotionSpy.last().first(), QPoint(10, 5)); // Let's pretend that the client has changed the extents of the client-side drop-shadow // but the frame geometry didn't change. QSignalSpy bufferGeometryChangedSpy(client, &Window::bufferGeometryChanged); QVERIFY(bufferGeometryChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(client, &Window::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->xdgSurface()->set_window_geometry(10, 20, 200, 100); Test::render(surface.data(), QSize(220, 140), Qt::blue); QVERIFY(bufferGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 0); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QCOMPARE(client->bufferGeometry().size(), QSize(220, 140)); // Move the pointer to (20, 50) relative to the upper left frame corner, which is located // at (10, 20) in the surface-local coordinates. Test::pointerMotion(client->pos() + QPoint(20, 50), timestamp++); QVERIFY(pointerMotionSpy.wait()); QCOMPARE(pointerMotionSpy.last().first(), QPoint(10, 20) + QPoint(20, 50)); // Destroy the xdg-toplevel surface. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testReentrantSetFrameGeometry() { // This test verifies that calling moveResize() from a slot connected directly // to the frameGeometryChanged() signal won't cause an infinite recursion. // Create an xdg-toplevel surface and wait for the compositor to catch up. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->pos(), QPoint(0, 0)); // Let's pretend that there is a script that really wants the client to be at (100, 100). connect(client, &Window::frameGeometryChanged, this, [client]() { client->moveResize(QRect(QPoint(100, 100), client->size())); }); // Trigger the lambda above. client->move(QPoint(40, 50)); // Eventually, the client will end up at (100, 100). QCOMPARE(client->pos(), QPoint(100, 100)); // Destroy the xdg-toplevel surface. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testDoubleMaximize() { // This test verifies that the case where a client issues two set_maximized() requests // separated by the initial commit is handled properly. // Create the test surface. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); shellSurface->set_maximized(); surface->commit(KWayland::Client::Surface::CommitFlag::None); // Wait for the compositor to respond with a configure event. QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); QSize size = toplevelConfigureRequestedSpy.last().at(0).toSize(); QCOMPARE(size, QSize(1280, 1024)); Test::XdgToplevel::States states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); // Send another set_maximized() request, but do not attach any buffer yet. shellSurface->set_maximized(); surface->commit(KWayland::Client::Surface::CommitFlag::None); // The compositor must respond with another configure event even if the state hasn't changed. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); size = toplevelConfigureRequestedSpy.last().at(0).toSize(); QCOMPARE(size, QSize(1280, 1024)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); } void TestXdgShellWindow::testDoubleFullscreenSeparatedByCommit() { // Some applications do weird things at startup and this is one of them. This test verifies // that the window will have good frame geometry if the client has issued several // xdg_toplevel.set_fullscreen requests and they are separated by a surface commit with // no attached buffer. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); // Tell the compositor that we want the window to be shown in fullscreen mode. shellSurface->set_fullscreen(nullptr); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); QVERIFY(toplevelConfigureRequestedSpy.last().at(1).value() & Test::XdgToplevel::State::Fullscreen); // Ask again. shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); surface->commit(KWayland::Client::Surface::CommitFlag::None); shellSurface->set_fullscreen(nullptr); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); QVERIFY(toplevelConfigureRequestedSpy.last().at(1).value() & Test::XdgToplevel::State::Fullscreen); // Map the window. shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client->isFullScreen()); QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); } void TestXdgShellWindow::testMaximizeHorizontal() { // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); // Wait for the initial configure event. Test::XdgToplevel::States states; QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Map the client. shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(800, 600), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(800, 600)); // We should receive a configure event when the client becomes active. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Maximize the test client in horizontal direction. workspace()->slotWindowMaximizeHorizontal(); QCOMPARE(client->requestedMaximizeMode(), MaximizeHorizontal); QCOMPARE(client->maximizeMode(), MaximizeRestore); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 600)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Draw contents of the maximized client. QSignalSpy geometryChangedSpy(client, &Window::geometryChanged); QVERIFY(geometryChangedSpy.isValid()); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(1280, 600), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(1280, 600)); QCOMPARE(client->requestedMaximizeMode(), MaximizeHorizontal); QCOMPARE(client->maximizeMode(), MaximizeHorizontal); // Restore the client. workspace()->slotWindowMaximizeHorizontal(); QCOMPARE(client->requestedMaximizeMode(), MaximizeRestore); QCOMPARE(client->maximizeMode(), MaximizeHorizontal); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(800, 600)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Draw contents of the restored client. shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(800, 600), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(800, 600)); QCOMPARE(client->requestedMaximizeMode(), MaximizeRestore); QCOMPARE(client->maximizeMode(), MaximizeRestore); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testMaximizeVertical() { // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); // Wait for the initial configure event. Test::XdgToplevel::States states; QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Map the client. shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(800, 600), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(800, 600)); // We should receive a configure event when the client becomes active. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Maximize the test client in vertical direction. workspace()->slotWindowMaximizeVertical(); QCOMPARE(client->requestedMaximizeMode(), MaximizeVertical); QCOMPARE(client->maximizeMode(), MaximizeRestore); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(800, 1024)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Draw contents of the maximized client. QSignalSpy geometryChangedSpy(client, &Window::geometryChanged); QVERIFY(geometryChangedSpy.isValid()); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(800, 1024), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(800, 1024)); QCOMPARE(client->requestedMaximizeMode(), MaximizeVertical); QCOMPARE(client->maximizeMode(), MaximizeVertical); // Restore the client. workspace()->slotWindowMaximizeVertical(); QCOMPARE(client->requestedMaximizeMode(), MaximizeRestore); QCOMPARE(client->maximizeMode(), MaximizeVertical); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(800, 600)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Draw contents of the restored client. shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(800, 600), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(800, 600)); QCOMPARE(client->requestedMaximizeMode(), MaximizeRestore); QCOMPARE(client->maximizeMode(), MaximizeRestore); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testMaximizeFull() { // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); surface->commit(KWayland::Client::Surface::CommitFlag::None); // Wait for the initial configure event. Test::XdgToplevel::States states; QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 1); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated)); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Map the client. shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Window *client = Test::renderAndWaitForShown(surface.data(), QSize(800, 600), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(800, 600)); // We should receive a configure event when the client becomes active. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 2); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated)); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Maximize the test client. workspace()->slotWindowMaximize(); QCOMPARE(client->requestedMaximizeMode(), MaximizeFull); QCOMPARE(client->maximizeMode(), MaximizeRestore); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 3); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized)); // Draw contents of the maximized client. QSignalSpy geometryChangedSpy(client, &Window::geometryChanged); QVERIFY(geometryChangedSpy.isValid()); shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(1280, 1024)); QCOMPARE(client->requestedMaximizeMode(), MaximizeFull); QCOMPARE(client->maximizeMode(), MaximizeFull); // Restore the client. workspace()->slotWindowMaximize(); QCOMPARE(client->requestedMaximizeMode(), MaximizeRestore); QCOMPARE(client->maximizeMode(), MaximizeFull); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(surfaceConfigureRequestedSpy.count(), 4); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(800, 600)); states = toplevelConfigureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized)); // Draw contents of the restored client. shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value()); Test::render(surface.data(), QSize(800, 600), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(800, 600)); QCOMPARE(client->requestedMaximizeMode(), MaximizeRestore); QCOMPARE(client->maximizeMode(), MaximizeRestore); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellWindow::testMaximizeAndChangeDecorationModeAfterInitialCommit() { // Ideally, the app would initialize the xdg-toplevel surface before the initial commit, but // many don't do it. They initialize the surface after the first commit. // This test verifies that the client will receive a configure event with correct size // if an xdg-toplevel surface is set maximized and decoration mode changes after initial commit. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer decoration(Test::createXdgToplevelDecorationV1(shellSurface.data())); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); // Commit the initial state. surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); // Request maximized mode and set decoration mode, i.e. perform late initialization. shellSurface->set_maximized(); decoration->set_mode(Test::XdgToplevelDecorationV1::mode_client_side); // The compositor will respond with a new configure event, which should contain maximized state. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); QCOMPARE(toplevelConfigureRequestedSpy.last().at(1).value(), Test::XdgToplevel::State::Maximized); } void TestXdgShellWindow::testFullScreenAndChangeDecorationModeAfterInitialCommit() { // Ideally, the app would initialize the xdg-toplevel surface before the initial commit, but // many don't do it. They initialize the surface after the first commit. // This test verifies that the client will receive a configure event with correct size // if an xdg-toplevel surface is set fullscreen and decoration mode changes after initial commit. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer decoration(Test::createXdgToplevelDecorationV1(shellSurface.data())); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); // Commit the initial state. surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); // Request fullscreen mode and set decoration mode, i.e. perform late initialization. shellSurface->set_fullscreen(nullptr); decoration->set_mode(Test::XdgToplevelDecorationV1::mode_client_side); // The compositor will respond with a new configure event, which should contain fullscreen state. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); QCOMPARE(toplevelConfigureRequestedSpy.last().at(1).value(), Test::XdgToplevel::State::Fullscreen); } void TestXdgShellWindow::testChangeDecorationModeAfterInitialCommit() { // This test verifies that the compositor will respond with a good configure event when // the decoration mode changes after the first surface commit but before the surface is mapped. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer decoration(Test::createXdgToplevelDecorationV1(shellSurface.data())); QSignalSpy decorationConfigureRequestedSpy(decoration.data(), &Test::XdgToplevelDecorationV1::configureRequested); QSignalSpy toplevelConfigureRequestedSpy(shellSurface.data(), &Test::XdgToplevel::configureRequested); QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested); // Perform the initial commit. surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); QCOMPARE(decorationConfigureRequestedSpy.last().at(0).value(), Test::XdgToplevelDecorationV1::mode_server_side); // Change decoration mode. decoration->set_mode(Test::XdgToplevelDecorationV1::mode_client_side); // The configure event should still have 0x0 size. QVERIFY(surfaceConfigureRequestedSpy.wait()); QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).value(), QSize(0, 0)); QCOMPARE(decorationConfigureRequestedSpy.last().at(0).value(), Test::XdgToplevelDecorationV1::mode_client_side); } WAYLANDTEST_MAIN(TestXdgShellWindow) #include "xdgshellwindow_test.moc"