File indexing completed on 2024-05-05 17:35:54

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2016 Martin Gräßlin <mgraesslin@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 #include "kwin_wayland_test.h"
0010 
0011 #include "core/output.h"
0012 #include "core/outputbackend.h"
0013 #include "cursor.h"
0014 #include "wayland/seat_interface.h"
0015 #include "wayland_server.h"
0016 #include "workspace.h"
0017 #include "x11window.h"
0018 #include <kwineffects.h>
0019 
0020 #include <KWayland/Client/compositor.h>
0021 #include <KWayland/Client/plasmawindowmanagement.h>
0022 #include <KWayland/Client/surface.h>
0023 // screenlocker
0024 #if KWIN_BUILD_SCREENLOCKER
0025 #include <KScreenLocker/KsldApp>
0026 #endif
0027 
0028 #include <QPainter>
0029 #include <QRasterWindow>
0030 
0031 #include <netwm.h>
0032 #include <xcb/xcb_icccm.h>
0033 
0034 namespace KWin
0035 {
0036 
0037 static const QString s_socketName = QStringLiteral("wayland_test_kwin_plasma-window-0");
0038 
0039 class PlasmaWindowTest : public QObject
0040 {
0041     Q_OBJECT
0042 private Q_SLOTS:
0043     void initTestCase();
0044     void init();
0045     void cleanup();
0046     void testCreateDestroyX11PlasmaWindow();
0047     void testInternalWindowNoPlasmaWindow();
0048     void testPopupWindowNoPlasmaWindow();
0049     void testLockScreenNoPlasmaWindow();
0050     void testDestroyedButNotUnmapped();
0051 
0052 private:
0053     KWayland::Client::PlasmaWindowManagement *m_windowManagement = nullptr;
0054     KWayland::Client::Compositor *m_compositor = nullptr;
0055 };
0056 
0057 void PlasmaWindowTest::initTestCase()
0058 {
0059     qRegisterMetaType<KWin::Window *>();
0060     QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
0061     QVERIFY(waylandServer()->init(s_socketName));
0062     QMetaObject::invokeMethod(kwinApp()->outputBackend(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(QVector<QRect>, QVector<QRect>() << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024)));
0063 
0064     kwinApp()->start();
0065     QVERIFY(applicationStartedSpy.wait());
0066     const auto outputs = workspace()->outputs();
0067     QCOMPARE(outputs.count(), 2);
0068     QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
0069     QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
0070     setenv("QT_QPA_PLATFORM", "wayland", true);
0071     setenv("QMLSCENE_DEVICE", "softwarecontext", true);
0072 }
0073 
0074 void PlasmaWindowTest::init()
0075 {
0076     QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::WindowManagement));
0077     m_windowManagement = Test::waylandWindowManagement();
0078     m_compositor = Test::waylandCompositor();
0079 
0080     workspace()->setActiveOutput(QPoint(640, 512));
0081     Cursors::self()->mouse()->setPos(QPoint(640, 512));
0082 }
0083 
0084 void PlasmaWindowTest::cleanup()
0085 {
0086     Test::destroyWaylandConnection();
0087 }
0088 
0089 void PlasmaWindowTest::testCreateDestroyX11PlasmaWindow()
0090 {
0091     // this test verifies that a PlasmaWindow gets unmapped on Client side when an X11 window is destroyed
0092     QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated);
0093 
0094     // create an xcb window
0095     struct XcbConnectionDeleter
0096     {
0097         void operator()(xcb_connection_t *pointer)
0098         {
0099             xcb_disconnect(pointer);
0100         }
0101     };
0102     std::unique_ptr<xcb_connection_t, XcbConnectionDeleter> c(xcb_connect(nullptr, nullptr));
0103     QVERIFY(!xcb_connection_has_error(c.get()));
0104     const QRect windowGeometry(0, 0, 100, 200);
0105     xcb_window_t windowId = xcb_generate_id(c.get());
0106     xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(),
0107                       windowGeometry.x(),
0108                       windowGeometry.y(),
0109                       windowGeometry.width(),
0110                       windowGeometry.height(),
0111                       0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr);
0112     xcb_size_hints_t hints;
0113     memset(&hints, 0, sizeof(hints));
0114     xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y());
0115     xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height());
0116     xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints);
0117     xcb_map_window(c.get(), windowId);
0118     xcb_flush(c.get());
0119 
0120     // we should get a window for it
0121     QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded);
0122     QVERIFY(windowCreatedSpy.wait());
0123     X11Window *window = windowCreatedSpy.first().first().value<X11Window *>();
0124     QVERIFY(window);
0125     QCOMPARE(window->window(), windowId);
0126     QVERIFY(window->isDecorated());
0127     QVERIFY(window->isActive());
0128     // verify that it gets the keyboard focus
0129     if (!window->surface()) {
0130         // we don't have a surface yet, so focused keyboard surface if set is not ours
0131         QVERIFY(!waylandServer()->seat()->focusedKeyboardSurface());
0132         QVERIFY(Test::waitForWaylandSurface(window));
0133     }
0134     QCOMPARE(waylandServer()->seat()->focusedKeyboardSurface(), window->surface());
0135 
0136     // now that should also give it to us on client side
0137     QVERIFY(plasmaWindowCreatedSpy.wait());
0138     QCOMPARE(plasmaWindowCreatedSpy.count(), 1);
0139     QCOMPARE(m_windowManagement->windows().count(), 1);
0140     auto pw = m_windowManagement->windows().first();
0141     QCOMPARE(pw->geometry(), window->frameGeometry());
0142     QSignalSpy geometryChangedSpy(pw, &KWayland::Client::PlasmaWindow::geometryChanged);
0143 
0144     QSignalSpy unmappedSpy(m_windowManagement->windows().first(), &KWayland::Client::PlasmaWindow::unmapped);
0145     QSignalSpy destroyedSpy(m_windowManagement->windows().first(), &QObject::destroyed);
0146 
0147     // now shade the window
0148     const QRectF geoBeforeShade = window->frameGeometry();
0149     QVERIFY(geoBeforeShade.isValid());
0150     QVERIFY(!geoBeforeShade.isEmpty());
0151     workspace()->slotWindowShade();
0152     QVERIFY(window->isShade());
0153     QVERIFY(window->frameGeometry() != geoBeforeShade);
0154     QVERIFY(geometryChangedSpy.wait());
0155     QCOMPARE(pw->geometry(), window->frameGeometry());
0156     // and unshade again
0157     workspace()->slotWindowShade();
0158     QVERIFY(!window->isShade());
0159     QCOMPARE(window->frameGeometry(), geoBeforeShade);
0160     QVERIFY(geometryChangedSpy.wait());
0161     QCOMPARE(pw->geometry(), geoBeforeShade);
0162 
0163     // and destroy the window again
0164     xcb_unmap_window(c.get(), windowId);
0165     xcb_flush(c.get());
0166 
0167     QSignalSpy windowClosedSpy(window, &X11Window::windowClosed);
0168     QVERIFY(windowClosedSpy.wait());
0169     xcb_destroy_window(c.get(), windowId);
0170     c.reset();
0171 
0172     QVERIFY(unmappedSpy.wait());
0173     QCOMPARE(unmappedSpy.count(), 1);
0174 
0175     QVERIFY(destroyedSpy.wait());
0176 }
0177 
0178 class HelperWindow : public QRasterWindow
0179 {
0180     Q_OBJECT
0181 public:
0182     HelperWindow();
0183     ~HelperWindow() override;
0184 
0185 protected:
0186     void paintEvent(QPaintEvent *event) override;
0187 };
0188 
0189 HelperWindow::HelperWindow()
0190     : QRasterWindow(nullptr)
0191 {
0192 }
0193 
0194 HelperWindow::~HelperWindow() = default;
0195 
0196 void HelperWindow::paintEvent(QPaintEvent *event)
0197 {
0198     QPainter p(this);
0199     p.fillRect(0, 0, width(), height(), Qt::red);
0200 }
0201 
0202 void PlasmaWindowTest::testInternalWindowNoPlasmaWindow()
0203 {
0204     // this test verifies that an internal window is not added as a PlasmaWindow
0205     QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated);
0206     HelperWindow win;
0207     win.setGeometry(0, 0, 100, 100);
0208     win.show();
0209 
0210     QVERIFY(!plasmaWindowCreatedSpy.wait(100));
0211 }
0212 
0213 void PlasmaWindowTest::testPopupWindowNoPlasmaWindow()
0214 {
0215     // this test verifies that a popup window is not added as a PlasmaWindow
0216     QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated);
0217 
0218     // first create the parent window
0219     std::unique_ptr<KWayland::Client::Surface> parentSurface(Test::createSurface());
0220     std::unique_ptr<Test::XdgToplevel> parentShellSurface(Test::createXdgToplevelSurface(parentSurface.get()));
0221     Window *parentClient = Test::renderAndWaitForShown(parentSurface.get(), QSize(100, 50), Qt::blue);
0222     QVERIFY(parentClient);
0223     QVERIFY(plasmaWindowCreatedSpy.wait());
0224     QCOMPARE(plasmaWindowCreatedSpy.count(), 1);
0225 
0226     // now let's create a popup window for it
0227     std::unique_ptr<Test::XdgPositioner> positioner(Test::createXdgPositioner());
0228     positioner->set_size(10, 10);
0229     positioner->set_anchor_rect(0, 0, 10, 10);
0230     positioner->set_anchor(Test::XdgPositioner::anchor_bottom_right);
0231     positioner->set_gravity(Test::XdgPositioner::gravity_bottom_right);
0232     std::unique_ptr<KWayland::Client::Surface> popupSurface(Test::createSurface());
0233     std::unique_ptr<Test::XdgPopup> popupShellSurface(Test::createXdgPopupSurface(popupSurface.get(), parentShellSurface->xdgSurface(), positioner.get()));
0234     Window *popupWindow = Test::renderAndWaitForShown(popupSurface.get(), QSize(10, 10), Qt::blue);
0235     QVERIFY(popupWindow);
0236     QVERIFY(!plasmaWindowCreatedSpy.wait(100));
0237     QCOMPARE(plasmaWindowCreatedSpy.count(), 1);
0238 
0239     // let's destroy the windows
0240     popupShellSurface.reset();
0241     QVERIFY(Test::waitForWindowDestroyed(popupWindow));
0242     parentShellSurface.reset();
0243     QVERIFY(Test::waitForWindowDestroyed(parentClient));
0244 }
0245 
0246 void PlasmaWindowTest::testLockScreenNoPlasmaWindow()
0247 {
0248 #if KWIN_BUILD_SCREENLOCKER
0249     // this test verifies that lock screen windows are not exposed to PlasmaWindow
0250     QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated);
0251 
0252     // this time we use a QSignalSpy on XdgShellClient as it'a a little bit more complex setup
0253     QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
0254     // lock
0255     ScreenLocker::KSldApp::self()->lock(ScreenLocker::EstablishLock::Immediate);
0256     QVERIFY(windowAddedSpy.wait());
0257     QVERIFY(windowAddedSpy.first().first().value<Window *>()->isLockScreen());
0258     // should not be sent to the window
0259     QVERIFY(plasmaWindowCreatedSpy.isEmpty());
0260     QVERIFY(!plasmaWindowCreatedSpy.wait(100));
0261 
0262     // fake unlock
0263     QSignalSpy lockStateChangedSpy(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged);
0264     const auto children = ScreenLocker::KSldApp::self()->children();
0265     for (auto it = children.begin(); it != children.end(); ++it) {
0266         if (qstrcmp((*it)->metaObject()->className(), "LogindIntegration") != 0) {
0267             continue;
0268         }
0269         QMetaObject::invokeMethod(*it, "requestUnlock");
0270         break;
0271     }
0272     QVERIFY(lockStateChangedSpy.wait());
0273     QVERIFY(!waylandServer()->isScreenLocked());
0274 #else
0275     QSKIP("KWin was built without lockscreen support");
0276 #endif
0277 }
0278 
0279 void PlasmaWindowTest::testDestroyedButNotUnmapped()
0280 {
0281     // this test verifies that also when a ShellSurface gets destroyed without a prior unmap
0282     // the PlasmaWindow gets destroyed on Client side
0283     QSignalSpy plasmaWindowCreatedSpy(m_windowManagement, &KWayland::Client::PlasmaWindowManagement::windowCreated);
0284 
0285     // first create the parent window
0286     std::unique_ptr<KWayland::Client::Surface> parentSurface(Test::createSurface());
0287     std::unique_ptr<Test::XdgToplevel> parentShellSurface(Test::createXdgToplevelSurface(parentSurface.get()));
0288     // map that window
0289     Test::render(parentSurface.get(), QSize(100, 50), Qt::blue);
0290     // this should create a plasma window
0291     QVERIFY(plasmaWindowCreatedSpy.wait());
0292     QCOMPARE(plasmaWindowCreatedSpy.count(), 1);
0293     auto window = plasmaWindowCreatedSpy.first().first().value<KWayland::Client::PlasmaWindow *>();
0294     QVERIFY(window);
0295     QSignalSpy destroyedSpy(window, &QObject::destroyed);
0296 
0297     // now destroy without an unmap
0298     parentShellSurface.reset();
0299     parentSurface.reset();
0300     QVERIFY(destroyedSpy.wait());
0301 }
0302 
0303 }
0304 
0305 WAYLANDTEST_MAIN(KWin::PlasmaWindowTest)
0306 #include "plasmawindow_test.moc"