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 "keyboard_input.h"
0015 #include "pointer_input.h"
0016 #include "wayland/seat_interface.h"
0017 #include "wayland/surface_interface.h"
0018 #include "wayland_server.h"
0019 #include "window.h"
0020 #include "workspace.h"
0021 
0022 #include <KWayland/Client/compositor.h>
0023 #include <KWayland/Client/keyboard.h>
0024 #include <KWayland/Client/pointer.h>
0025 #include <KWayland/Client/pointerconstraints.h>
0026 #include <KWayland/Client/region.h>
0027 #include <KWayland/Client/seat.h>
0028 #include <KWayland/Client/shm_pool.h>
0029 #include <KWayland/Client/surface.h>
0030 
0031 #include <linux/input.h>
0032 
0033 #include <functional>
0034 
0035 using namespace KWin;
0036 
0037 typedef std::function<QPointF(const QRectF &)> PointerFunc;
0038 Q_DECLARE_METATYPE(PointerFunc)
0039 
0040 static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_constraints-0");
0041 
0042 class TestPointerConstraints : public QObject
0043 {
0044     Q_OBJECT
0045 private Q_SLOTS:
0046     void initTestCase();
0047     void init();
0048     void cleanup();
0049 
0050     void testConfinedPointer_data();
0051     void testConfinedPointer();
0052     void testLockedPointer();
0053     void testCloseWindowWithLockedPointer();
0054 };
0055 
0056 void TestPointerConstraints::initTestCase()
0057 {
0058     qRegisterMetaType<PointerFunc>();
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     // set custom config which disables the OnScreenNotification
0065     KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
0066     KConfigGroup group = config->group("OnScreenNotification");
0067     group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml"));
0068     group.sync();
0069 
0070     kwinApp()->setConfig(config);
0071 
0072     kwinApp()->start();
0073     QVERIFY(applicationStartedSpy.wait());
0074     const auto outputs = workspace()->outputs();
0075     QCOMPARE(outputs.count(), 2);
0076     QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
0077     QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
0078 }
0079 
0080 void TestPointerConstraints::init()
0081 {
0082     QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::PointerConstraints));
0083     QVERIFY(Test::waitForWaylandPointer());
0084 
0085     workspace()->setActiveOutput(QPoint(640, 512));
0086     KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512));
0087 }
0088 
0089 void TestPointerConstraints::cleanup()
0090 {
0091     Test::destroyWaylandConnection();
0092 }
0093 
0094 void TestPointerConstraints::testConfinedPointer_data()
0095 {
0096     QTest::addColumn<PointerFunc>("positionFunction");
0097     QTest::addColumn<int>("xOffset");
0098     QTest::addColumn<int>("yOffset");
0099     PointerFunc bottomLeft = [](const QRectF &rect) {
0100         return rect.toRect().bottomLeft();
0101     };
0102     PointerFunc bottomRight = [](const QRectF &rect) {
0103         return rect.toRect().bottomRight();
0104     };
0105     PointerFunc topRight = [](const QRectF &rect) {
0106         return rect.toRect().topRight();
0107     };
0108     PointerFunc topLeft = [](const QRectF &rect) {
0109         return rect.toRect().topLeft();
0110     };
0111 
0112     QTest::newRow("XdgWmBase - bottomLeft") << bottomLeft << -1 << 1;
0113     QTest::newRow("XdgWmBase - bottomRight") << bottomRight << 1 << 1;
0114     QTest::newRow("XdgWmBase - topLeft") << topLeft << -1 << -1;
0115     QTest::newRow("XdgWmBase - topRight") << topRight << 1 << -1;
0116 }
0117 
0118 void TestPointerConstraints::testConfinedPointer()
0119 {
0120     // this test sets up a Surface with a confined pointer
0121     // simple interaction test to verify that the pointer gets confined
0122     std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
0123     std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
0124     std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
0125     std::unique_ptr<KWayland::Client::ConfinedPointer> confinedPointer(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot));
0126     QSignalSpy confinedSpy(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined);
0127     QSignalSpy unconfinedSpy(confinedPointer.get(), &KWayland::Client::ConfinedPointer::unconfined);
0128 
0129     // now map the window
0130     auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
0131     QVERIFY(window);
0132     if (window->pos() == QPoint(0, 0)) {
0133         window->move(QPoint(1, 1));
0134     }
0135     QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos()));
0136 
0137     // now let's confine
0138     QCOMPARE(input()->pointer()->isConstrained(), false);
0139     KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center());
0140     QCOMPARE(input()->pointer()->isConstrained(), true);
0141     QVERIFY(confinedSpy.wait());
0142 
0143     // picking a position outside the window geometry should not move pointer
0144     QSignalSpy pointerPositionChangedSpy(input(), &InputRedirection::globalPointerChanged);
0145     KWin::Cursors::self()->mouse()->setPos(QPoint(512, 512));
0146     QVERIFY(pointerPositionChangedSpy.isEmpty());
0147     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
0148 
0149     // TODO: test relative motion
0150     QFETCH(PointerFunc, positionFunction);
0151     const QPointF position = positionFunction(window->frameGeometry());
0152     KWin::Cursors::self()->mouse()->setPos(position);
0153     QCOMPARE(pointerPositionChangedSpy.count(), 1);
0154     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
0155     // moving one to right should not be possible
0156     QFETCH(int, xOffset);
0157     KWin::Cursors::self()->mouse()->setPos(position + QPoint(xOffset, 0));
0158     QCOMPARE(pointerPositionChangedSpy.count(), 1);
0159     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
0160     // moving one to bottom should not be possible
0161     QFETCH(int, yOffset);
0162     KWin::Cursors::self()->mouse()->setPos(position + QPoint(0, yOffset));
0163     QCOMPARE(pointerPositionChangedSpy.count(), 1);
0164     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
0165 
0166     // modifier + click should be ignored
0167     // first ensure the settings are ok
0168     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
0169     group.writeEntry("CommandAllKey", QStringLiteral("Meta"));
0170     group.writeEntry("CommandAll1", "Move");
0171     group.writeEntry("CommandAll2", "Move");
0172     group.writeEntry("CommandAll3", "Move");
0173     group.writeEntry("CommandAllWheel", "change opacity");
0174     group.sync();
0175     workspace()->slotReconfigure();
0176     QCOMPARE(options->commandAllModifier(), Qt::MetaModifier);
0177     QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove);
0178     QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove);
0179     QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove);
0180 
0181     quint32 timestamp = 1;
0182     Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
0183     Test::pointerButtonPressed(BTN_LEFT, timestamp++);
0184     QVERIFY(!window->isInteractiveMove());
0185     Test::pointerButtonReleased(BTN_LEFT, timestamp++);
0186 
0187     // set the opacity to 0.5
0188     window->setOpacity(0.5);
0189     QCOMPARE(window->opacity(), 0.5);
0190 
0191     // pointer is confined so shortcut should not work
0192     Test::pointerAxisVertical(-5, timestamp++);
0193     QCOMPARE(window->opacity(), 0.5);
0194     Test::pointerAxisVertical(5, timestamp++);
0195     QCOMPARE(window->opacity(), 0.5);
0196 
0197     Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
0198 
0199     // deactivate the window, this should unconfine
0200     workspace()->activateWindow(nullptr);
0201     QVERIFY(unconfinedSpy.wait());
0202     QCOMPARE(input()->pointer()->isConstrained(), false);
0203 
0204     // reconfine pointer (this time with persistent life time)
0205     confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
0206     QSignalSpy confinedSpy2(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined);
0207     QSignalSpy unconfinedSpy2(confinedPointer.get(), &KWayland::Client::ConfinedPointer::unconfined);
0208 
0209     // activate it again, this confines again
0210     workspace()->activateWindow(static_cast<Window *>(input()->pointer()->focus()));
0211     QVERIFY(confinedSpy2.wait());
0212     QCOMPARE(input()->pointer()->isConstrained(), true);
0213 
0214     // deactivate the window one more time with the persistent life time constraint, this should unconfine
0215     workspace()->activateWindow(nullptr);
0216     QVERIFY(unconfinedSpy2.wait());
0217     QCOMPARE(input()->pointer()->isConstrained(), false);
0218     // activate it again, this confines again
0219     workspace()->activateWindow(static_cast<Window *>(input()->pointer()->focus()));
0220     QVERIFY(confinedSpy2.wait());
0221     QCOMPARE(input()->pointer()->isConstrained(), true);
0222 
0223     // create a second window and move it above our constrained window
0224     std::unique_ptr<KWayland::Client::Surface> surface2(Test::createSurface());
0225     std::unique_ptr<Test::XdgToplevel> shellSurface2(Test::createXdgToplevelSurface(surface2.get()));
0226     auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(1280, 1024), Qt::blue);
0227     QVERIFY(c2);
0228     QVERIFY(unconfinedSpy2.wait());
0229     // and unmapping the second window should confine again
0230     shellSurface2.reset();
0231     surface2.reset();
0232     QVERIFY(confinedSpy2.wait());
0233 
0234     // let's set a region which results in unconfined
0235     auto r = Test::waylandCompositor()->createRegion(QRegion(2, 2, 3, 3));
0236     confinedPointer->setRegion(r.get());
0237     surface->commit(KWayland::Client::Surface::CommitFlag::None);
0238     QVERIFY(unconfinedSpy2.wait());
0239     QCOMPARE(input()->pointer()->isConstrained(), false);
0240     // and set a full region again, that should confine
0241     confinedPointer->setRegion(nullptr);
0242     surface->commit(KWayland::Client::Surface::CommitFlag::None);
0243     QVERIFY(confinedSpy2.wait());
0244     QCOMPARE(input()->pointer()->isConstrained(), true);
0245 
0246     // delete pointer confine
0247     confinedPointer.reset(nullptr);
0248     Test::flushWaylandConnection();
0249 
0250     QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWaylandServer::SurfaceInterface::pointerConstraintsChanged);
0251     QVERIFY(constraintsChangedSpy.wait());
0252 
0253     // should be unconfined
0254     QCOMPARE(input()->pointer()->isConstrained(), false);
0255 
0256     // confine again
0257     confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
0258     QSignalSpy confinedSpy3(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined);
0259     QVERIFY(confinedSpy3.wait());
0260     QCOMPARE(input()->pointer()->isConstrained(), true);
0261 
0262     // and now unmap
0263     shellSurface.reset();
0264     surface.reset();
0265     QVERIFY(Test::waitForWindowDestroyed(window));
0266     QCOMPARE(input()->pointer()->isConstrained(), false);
0267 }
0268 
0269 void TestPointerConstraints::testLockedPointer()
0270 {
0271     // this test sets up a Surface with a locked pointer
0272     // simple interaction test to verify that the pointer gets locked
0273     // the various ways to unlock are not tested as that's already verified by testConfinedPointer
0274     std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
0275     std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
0276     std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
0277     std::unique_ptr<KWayland::Client::LockedPointer> lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot));
0278     QSignalSpy lockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::locked);
0279     QSignalSpy unlockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::unlocked);
0280 
0281     // now map the window
0282     auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
0283     QVERIFY(window);
0284     QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos()));
0285 
0286     // now let's lock
0287     QCOMPARE(input()->pointer()->isConstrained(), false);
0288     KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center());
0289     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
0290     QCOMPARE(input()->pointer()->isConstrained(), true);
0291     QVERIFY(lockedSpy.wait());
0292 
0293     // try to move the pointer
0294     // TODO: add relative pointer
0295     KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center() + QPoint(1, 1));
0296     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
0297 
0298     // deactivate the window, this should unlock
0299     workspace()->activateWindow(nullptr);
0300     QCOMPARE(input()->pointer()->isConstrained(), false);
0301     QVERIFY(unlockedSpy.wait());
0302 
0303     // moving cursor should be allowed again
0304     KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center() + QPoint(1, 1));
0305     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1));
0306 
0307     lockedPointer.reset(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
0308     QSignalSpy lockedSpy2(lockedPointer.get(), &KWayland::Client::LockedPointer::locked);
0309 
0310     // activate the window again, this should lock again
0311     workspace()->activateWindow(static_cast<Window *>(input()->pointer()->focus()));
0312     QVERIFY(lockedSpy2.wait());
0313     QCOMPARE(input()->pointer()->isConstrained(), true);
0314 
0315     // try to move the pointer
0316     QCOMPARE(input()->pointer()->isConstrained(), true);
0317     KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center());
0318     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1));
0319 
0320     // delete pointer lock
0321     lockedPointer.reset(nullptr);
0322     Test::flushWaylandConnection();
0323 
0324     QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWaylandServer::SurfaceInterface::pointerConstraintsChanged);
0325     QVERIFY(constraintsChangedSpy.wait());
0326 
0327     // moving cursor should be allowed again
0328     QCOMPARE(input()->pointer()->isConstrained(), false);
0329     KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center());
0330     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
0331 }
0332 
0333 void TestPointerConstraints::testCloseWindowWithLockedPointer()
0334 {
0335     // test case which verifies that the pointer gets unlocked when the window for it gets closed
0336     std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
0337     std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
0338     std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
0339     std::unique_ptr<KWayland::Client::LockedPointer> lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot));
0340     QSignalSpy lockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::locked);
0341     QSignalSpy unlockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::unlocked);
0342 
0343     // now map the window
0344     auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
0345     QVERIFY(window);
0346     QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos()));
0347 
0348     // now let's lock
0349     QCOMPARE(input()->pointer()->isConstrained(), false);
0350     KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center());
0351     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
0352     QCOMPARE(input()->pointer()->isConstrained(), true);
0353     QVERIFY(lockedSpy.wait());
0354 
0355     // close the window
0356     shellSurface.reset();
0357     surface.reset();
0358     // this should result in unlocked
0359     QVERIFY(unlockedSpy.wait());
0360     QCOMPARE(input()->pointer()->isConstrained(), false);
0361 }
0362 
0363 WAYLANDTEST_MAIN(TestPointerConstraints)
0364 #include "pointer_constraints_test.moc"