diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -36,6 +36,7 @@ integrationTest(NAME testXClipboardSync SRCS xclipboardsync_test.cpp) integrationTest(NAME testSceneQPainter SRCS scene_qpainter_test.cpp) integrationTest(NAME testNoXdgRuntimeDir SRCS no_xdg_runtime_dir_test.cpp) +integrationTest(NAME testScreenChanges SRCS screen_changes_test.cpp) if (XCB_ICCCM_FOUND) integrationTest(NAME testMoveResize SRCS move_resize_window_test.cpp LIBS XCB::ICCCM) diff --git a/autotests/integration/screen_changes_test.cpp b/autotests/integration/screen_changes_test.cpp new file mode 100644 --- /dev/null +++ b/autotests/integration/screen_changes_test.cpp @@ -0,0 +1,176 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" + +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_screen_changes-0"); + +class ScreenChangesTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testScreenAddRemove(); +}; + +void ScreenChangesTest::initTestCase() +{ + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void ScreenChangesTest::init() +{ + QVERIFY(Test::setupWaylandConnection(s_socketName)); + + screens()->setCurrent(0); + KWin::Cursor::setPos(QPoint(640, 512)); +} + +void ScreenChangesTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void ScreenChangesTest::testScreenAddRemove() +{ + // this test verifies that when a new screen is added it gets synced to Wayland + + // first create a registry to get signals about Outputs announced/removed + Registry registry; + QSignalSpy allAnnounced(®istry, &Registry::interfacesAnnounced); + QVERIFY(allAnnounced.isValid()); + QSignalSpy outputAnnouncedSpy(®istry, &Registry::outputAnnounced); + QVERIFY(outputAnnouncedSpy.isValid()); + QSignalSpy outputRemovedSpy(®istry, &Registry::outputRemoved); + QVERIFY(outputRemovedSpy.isValid()); + registry.create(Test::waylandConnection()); + QVERIFY(registry.isValid()); + registry.setup(); + QVERIFY(allAnnounced.wait()); + + // should be one output + QCOMPARE(screens()->count(), 1); + QCOMPARE(outputAnnouncedSpy.count(), 1); + const quint32 firstOutputId = outputAnnouncedSpy.first().first().value(); + QVERIFY(firstOutputId != 0u); + outputAnnouncedSpy.clear(); + + // let's announce a new output + QSignalSpy screensChangedSpy(screens(), &Screens::changed); + QVERIFY(screensChangedSpy.isValid()); + const QVector geometries{QRect(0, 0, 1280, 1024), QRect(1280, 0, 1280, 1024)}; + QMetaObject::invokeMethod(kwinApp()->platform(), "outputGeometriesChanged", + Qt::DirectConnection, + Q_ARG(QVector, geometries)); + QVERIFY(screensChangedSpy.wait()); + QCOMPARE(screensChangedSpy.count(), 1); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), geometries.at(0)); + QCOMPARE(screens()->geometry(1), geometries.at(1)); + + // this should result in it getting announced, two new outputs are added... + QVERIFY(outputAnnouncedSpy.wait()); + if (outputAnnouncedSpy.count() < 2) { + QVERIFY(outputAnnouncedSpy.wait()); + } + QCOMPARE(outputAnnouncedSpy.count(), 2); + // ... and afterward the previous output gets removed + if (outputRemovedSpy.isEmpty()) { + QVERIFY(outputRemovedSpy.wait()); + } + QCOMPARE(outputRemovedSpy.count(), 1); + QCOMPARE(outputRemovedSpy.first().first().value(), firstOutputId); + + // let's wait a little bit to ensure we don't get more events + QTest::qWait(100); + QCOMPARE(outputAnnouncedSpy.count(), 2); + QCOMPARE(outputRemovedSpy.count(), 1); + + // let's create the output objects to ensure they are correct + QScopedPointer o1(registry.createOutput(outputAnnouncedSpy.first().first().value(), outputAnnouncedSpy.first().last().value())); + QVERIFY(o1->isValid()); + QSignalSpy o1ChangedSpy(o1.data(), &Output::changed); + QVERIFY(o1ChangedSpy.isValid()); + QVERIFY(o1ChangedSpy.wait()); + QCOMPARE(o1->geometry(), geometries.at(0)); + QScopedPointer o2(registry.createOutput(outputAnnouncedSpy.last().first().value(), outputAnnouncedSpy.last().last().value())); + QVERIFY(o2->isValid()); + QSignalSpy o2ChangedSpy(o2.data(), &Output::changed); + QVERIFY(o2ChangedSpy.isValid()); + QVERIFY(o2ChangedSpy.wait()); + QCOMPARE(o2->geometry(), geometries.at(1)); + + // now let's try to remove one output again + outputAnnouncedSpy.clear(); + outputRemovedSpy.clear(); + screensChangedSpy.clear(); + + QSignalSpy o1RemovedSpy(o1.data(), &Output::removed); + QVERIFY(o1RemovedSpy.isValid()); + QSignalSpy o2RemovedSpy(o2.data(), &Output::removed); + QVERIFY(o2RemovedSpy.isValid()); + + const QVector geometries2{QRect(0, 0, 1280, 1024)}; + QMetaObject::invokeMethod(kwinApp()->platform(), "outputGeometriesChanged", + Qt::DirectConnection, + Q_ARG(QVector, geometries2)); + QVERIFY(screensChangedSpy.wait()); + QCOMPARE(screensChangedSpy.count(), 1); + QCOMPARE(screens()->count(), 1); + QCOMPARE(screens()->geometry(0), geometries2.at(0)); + + QVERIFY(outputAnnouncedSpy.wait()); + QCOMPARE(outputAnnouncedSpy.count(), 1); + if (o1RemovedSpy.isEmpty()) { + QVERIFY(o1RemovedSpy.wait()); + } + if (o2RemovedSpy.isEmpty()) { + QVERIFY(o2RemovedSpy.wait()); + } + // now wait a bit to ensure we don't get more events + QTest::qWait(100); + QCOMPARE(outputAnnouncedSpy.count(), 1); + QCOMPARE(o1RemovedSpy.count(), 1); + QCOMPARE(o2RemovedSpy.count(), 1); + QCOMPARE(outputRemovedSpy.count(), 2); +} + +WAYLANDTEST_MAIN(ScreenChangesTest) +#include "screen_changes_test.moc" diff --git a/plugins/platforms/virtual/screens_virtual.cpp b/plugins/platforms/virtual/screens_virtual.cpp --- a/plugins/platforms/virtual/screens_virtual.cpp +++ b/plugins/platforms/virtual/screens_virtual.cpp @@ -38,9 +38,13 @@ this, &VirtualScreens::startChangedTimer); connect(m_backend, &VirtualBackend::outputGeometriesChanged, this, [this] (const QVector &geometries) { - // TODO: update Wayland Output + const int oldCount = m_geometries.count(); m_geometries = geometries; - emit changed(); + if (oldCount != m_geometries.count()) { + setCount(m_geometries.count()); + } else { + emit changed(); + } } ); updateCount(); diff --git a/wayland_server.h b/wayland_server.h --- a/wayland_server.h +++ b/wayland_server.h @@ -172,6 +172,7 @@ void setupX11ClipboardSync(); void shellClientShown(Toplevel *t); void initOutputs(); + void syncOutputsToWayland(); quint16 createClientId(KWayland::Server::ClientConnection *c); void destroyInternalConnection(); void configurationChangeRequested(KWayland::Server::OutputConfigurationInterface *config); diff --git a/wayland_server.cpp b/wayland_server.cpp --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -330,6 +330,22 @@ if (kwinApp()->platform()->handlesOutputs()) { return; } + syncOutputsToWayland(); + connect(screens(), &Screens::changed, this, + [this] { + // when screens change we need to sync this to Wayland. + // Unfortunately we don't have much information and cannot properly match a KWin screen + // to a Wayland screen. + // Thus we just recreate all outputs and delete the old ones + const auto outputs = m_display->outputs(); + syncOutputsToWayland(); + qDeleteAll(outputs); + } + ); +} + +void WaylandServer::syncOutputsToWayland() +{ Screens *s = screens(); Q_ASSERT(s); for (int i = 0; i < s->count(); ++i) {