File indexing completed on 2025-03-23 13:48:02
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"