/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2016 Martin Gräßlin SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "core/output.h" #include "core/platform.h" #include "cursor.h" #include "keyboard_input.h" #include "pointer_input.h" #include "wayland/seat_interface.h" #include "wayland/surface_interface.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include using namespace KWin; using namespace KWayland::Client; typedef std::function PointerFunc; Q_DECLARE_METATYPE(PointerFunc) static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_constraints-0"); class TestPointerConstraints : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testConfinedPointer_data(); void testConfinedPointer(); void testLockedPointer(); void testCloseWindowWithLockedPointer(); }; void TestPointerConstraints::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); // set custom config which disables the OnScreenNotification KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); KConfigGroup group = config->group("OnScreenNotification"); group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); group.sync(); kwinApp()->setConfig(config); kwinApp()->start(); QVERIFY(applicationStartedSpy.wait()); const auto outputs = workspace()->outputs(); QCOMPARE(outputs.count(), 2); QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024)); } void TestPointerConstraints::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::PointerConstraints)); QVERIFY(Test::waitForWaylandPointer()); workspace()->setActiveOutput(QPoint(640, 512)); KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); } void TestPointerConstraints::cleanup() { Test::destroyWaylandConnection(); } void TestPointerConstraints::testConfinedPointer_data() { QTest::addColumn("positionFunction"); QTest::addColumn("xOffset"); QTest::addColumn("yOffset"); PointerFunc bottomLeft = [](const QRectF &rect) { return rect.toRect().bottomLeft(); }; PointerFunc bottomRight = [](const QRectF &rect) { return rect.toRect().bottomRight(); }; PointerFunc topRight = [](const QRectF &rect) { return rect.toRect().topRight(); }; PointerFunc topLeft = [](const QRectF &rect) { return rect.toRect().topLeft(); }; QTest::newRow("XdgWmBase - bottomLeft") << bottomLeft << -1 << 1; QTest::newRow("XdgWmBase - bottomRight") << bottomRight << 1 << 1; QTest::newRow("XdgWmBase - topLeft") << topLeft << -1 << -1; QTest::newRow("XdgWmBase - topRight") << topRight << 1 << -1; } void TestPointerConstraints::testConfinedPointer() { // this test sets up a Surface with a confined pointer // simple interaction test to verify that the pointer gets confined std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); std::unique_ptr pointer(Test::waylandSeat()->createPointer()); std::unique_ptr confinedPointer(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, PointerConstraints::LifeTime::OneShot)); QSignalSpy confinedSpy(confinedPointer.get(), &ConfinedPointer::confined); QSignalSpy unconfinedSpy(confinedPointer.get(), &ConfinedPointer::unconfined); // now map the window auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); QVERIFY(window); if (window->pos() == QPoint(0, 0)) { window->move(QPoint(1, 1)); } QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); // now let's confine QCOMPARE(input()->pointer()->isConstrained(), false); KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center()); QCOMPARE(input()->pointer()->isConstrained(), true); QVERIFY(confinedSpy.wait()); // picking a position outside the window geometry should not move pointer QSignalSpy pointerPositionChangedSpy(input(), &InputRedirection::globalPointerChanged); KWin::Cursors::self()->mouse()->setPos(QPoint(512, 512)); QVERIFY(pointerPositionChangedSpy.isEmpty()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); // TODO: test relative motion QFETCH(PointerFunc, positionFunction); const QPointF position = positionFunction(window->frameGeometry()); KWin::Cursors::self()->mouse()->setPos(position); QCOMPARE(pointerPositionChangedSpy.count(), 1); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position); // moving one to right should not be possible QFETCH(int, xOffset); KWin::Cursors::self()->mouse()->setPos(position + QPoint(xOffset, 0)); QCOMPARE(pointerPositionChangedSpy.count(), 1); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position); // moving one to bottom should not be possible QFETCH(int, yOffset); KWin::Cursors::self()->mouse()->setPos(position + QPoint(0, yOffset)); QCOMPARE(pointerPositionChangedSpy.count(), 1); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position); // modifier + click should be ignored // first ensure the settings are ok KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", QStringLiteral("Meta")); group.writeEntry("CommandAll1", "Move"); group.writeEntry("CommandAll2", "Move"); group.writeEntry("CommandAll3", "Move"); group.writeEntry("CommandAllWheel", "change opacity"); group.sync(); workspace()->slotReconfigure(); QCOMPARE(options->commandAllModifier(), Qt::MetaModifier); QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); quint32 timestamp = 1; Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(!window->isInteractiveMove()); Test::pointerButtonReleased(BTN_LEFT, timestamp++); // set the opacity to 0.5 window->setOpacity(0.5); QCOMPARE(window->opacity(), 0.5); // pointer is confined so shortcut should not work Test::pointerAxisVertical(-5, timestamp++); QCOMPARE(window->opacity(), 0.5); Test::pointerAxisVertical(5, timestamp++); QCOMPARE(window->opacity(), 0.5); Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); // deactivate the window, this should unconfine workspace()->activateWindow(nullptr); QVERIFY(unconfinedSpy.wait()); QCOMPARE(input()->pointer()->isConstrained(), false); // reconfine pointer (this time with persistent life time) confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, PointerConstraints::LifeTime::Persistent)); QSignalSpy confinedSpy2(confinedPointer.get(), &ConfinedPointer::confined); QSignalSpy unconfinedSpy2(confinedPointer.get(), &ConfinedPointer::unconfined); // activate it again, this confines again workspace()->activateWindow(static_cast(input()->pointer()->focus())); QVERIFY(confinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // deactivate the window one more time with the persistent life time constraint, this should unconfine workspace()->activateWindow(nullptr); QVERIFY(unconfinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), false); // activate it again, this confines again workspace()->activateWindow(static_cast(input()->pointer()->focus())); QVERIFY(confinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // create a second window and move it above our constrained window std::unique_ptr surface2(Test::createSurface()); std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(1280, 1024), Qt::blue); QVERIFY(c2); QVERIFY(unconfinedSpy2.wait()); // and unmapping the second window should confine again shellSurface2.reset(); surface2.reset(); QVERIFY(confinedSpy2.wait()); // let's set a region which results in unconfined auto r = Test::waylandCompositor()->createRegion(QRegion(2, 2, 3, 3)); confinedPointer->setRegion(r.get()); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(unconfinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), false); // and set a full region again, that should confine confinedPointer->setRegion(nullptr); surface->commit(KWayland::Client::Surface::CommitFlag::None); QVERIFY(confinedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // delete pointer confine confinedPointer.reset(nullptr); Test::flushWaylandConnection(); QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWaylandServer::SurfaceInterface::pointerConstraintsChanged); QVERIFY(constraintsChangedSpy.wait()); // should be unconfined QCOMPARE(input()->pointer()->isConstrained(), false); // confine again confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, PointerConstraints::LifeTime::Persistent)); QSignalSpy confinedSpy3(confinedPointer.get(), &ConfinedPointer::confined); QVERIFY(confinedSpy3.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // and now unmap shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(window)); QCOMPARE(input()->pointer()->isConstrained(), false); } void TestPointerConstraints::testLockedPointer() { // this test sets up a Surface with a locked pointer // simple interaction test to verify that the pointer gets locked // the various ways to unlock are not tested as that's already verified by testConfinedPointer std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); std::unique_ptr pointer(Test::waylandSeat()->createPointer()); std::unique_ptr lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, PointerConstraints::LifeTime::OneShot)); QSignalSpy lockedSpy(lockedPointer.get(), &LockedPointer::locked); QSignalSpy unlockedSpy(lockedPointer.get(), &LockedPointer::unlocked); // now map the window auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); QVERIFY(window); QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); // now let's lock QCOMPARE(input()->pointer()->isConstrained(), false); KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); QCOMPARE(input()->pointer()->isConstrained(), true); QVERIFY(lockedSpy.wait()); // try to move the pointer // TODO: add relative pointer KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center() + QPoint(1, 1)); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); // deactivate the window, this should unlock workspace()->activateWindow(nullptr); QCOMPARE(input()->pointer()->isConstrained(), false); QVERIFY(unlockedSpy.wait()); // moving cursor should be allowed again KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center() + QPoint(1, 1)); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1)); lockedPointer.reset(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, PointerConstraints::LifeTime::Persistent)); QSignalSpy lockedSpy2(lockedPointer.get(), &LockedPointer::locked); // activate the window again, this should lock again workspace()->activateWindow(static_cast(input()->pointer()->focus())); QVERIFY(lockedSpy2.wait()); QCOMPARE(input()->pointer()->isConstrained(), true); // try to move the pointer QCOMPARE(input()->pointer()->isConstrained(), true); KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1)); // delete pointer lock lockedPointer.reset(nullptr); Test::flushWaylandConnection(); QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWaylandServer::SurfaceInterface::pointerConstraintsChanged); QVERIFY(constraintsChangedSpy.wait()); // moving cursor should be allowed again QCOMPARE(input()->pointer()->isConstrained(), false); KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); } void TestPointerConstraints::testCloseWindowWithLockedPointer() { // test case which verifies that the pointer gets unlocked when the window for it gets closed std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); std::unique_ptr pointer(Test::waylandSeat()->createPointer()); std::unique_ptr lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, PointerConstraints::LifeTime::OneShot)); QSignalSpy lockedSpy(lockedPointer.get(), &LockedPointer::locked); QSignalSpy unlockedSpy(lockedPointer.get(), &LockedPointer::unlocked); // now map the window auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue); QVERIFY(window); QVERIFY(!window->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos())); // now let's lock QCOMPARE(input()->pointer()->isConstrained(), false); KWin::Cursors::self()->mouse()->setPos(window->frameGeometry().center()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center()); QCOMPARE(input()->pointer()->isConstrained(), true); QVERIFY(lockedSpy.wait()); // close the window shellSurface.reset(); surface.reset(); // this should result in unlocked QVERIFY(unlockedSpy.wait()); QCOMPARE(input()->pointer()->isConstrained(), false); } WAYLANDTEST_MAIN(TestPointerConstraints) #include "pointer_constraints_test.moc"