1 /*
2     KWin - the KDE window manager
3     This file is part of the KDE project.
4 
5     SPDX-FileCopyrightText: 2016 Martin Gräßlin <mgraesslin@kde.org>
6 
7     SPDX-License-Identifier: GPL-2.0-or-later
8 */
9 #include "kwin_wayland_test.h"
10 #include "abstract_client.h"
11 #include "abstract_output.h"
12 #include "cursor.h"
13 #include "keyboard_input.h"
14 #include "platform.h"
15 #include "pointer_input.h"
16 #include "screens.h"
17 #include "wayland_server.h"
18 #include "workspace.h"
19 
20 #include <KWayland/Client/compositor.h>
21 #include <KWayland/Client/keyboard.h>
22 #include <KWayland/Client/pointer.h>
23 #include <KWayland/Client/pointerconstraints.h>
24 #include <KWayland/Client/region.h>
25 #include <KWayland/Client/seat.h>
26 #include <KWayland/Client/shm_pool.h>
27 #include <KWayland/Client/surface.h>
28 #include <KWaylandServer/seat_interface.h>
29 #include <KWaylandServer/surface_interface.h>
30 
31 #include <linux/input.h>
32 
33 #include <functional>
34 
35 using namespace KWin;
36 using namespace KWayland::Client;
37 
38 typedef std::function<QPoint(const QRect&)> PointerFunc;
39 Q_DECLARE_METATYPE(PointerFunc)
40 
41 static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_constraints-0");
42 
43 class TestPointerConstraints : public QObject
44 {
45     Q_OBJECT
46 private Q_SLOTS:
47     void initTestCase();
48     void init();
49     void cleanup();
50 
51     void testConfinedPointer_data();
52     void testConfinedPointer();
53     void testLockedPointer();
54     void testCloseWindowWithLockedPointer();
55 };
56 
initTestCase()57 void TestPointerConstraints::initTestCase()
58 {
59     qRegisterMetaType<PointerFunc>();
60     qRegisterMetaType<KWin::AbstractClient*>();
61     QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
62     QVERIFY(applicationStartedSpy.isValid());
63     kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024));
64     QVERIFY(waylandServer()->init(s_socketName));
65     QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2));
66 
67     // set custom config which disables the OnScreenNotification
68     KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
69     KConfigGroup group = config->group("OnScreenNotification");
70     group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml"));
71     group.sync();
72 
73     kwinApp()->setConfig(config);
74 
75 
76     kwinApp()->start();
77     QVERIFY(applicationStartedSpy.wait());
78     const auto outputs = kwinApp()->platform()->enabledOutputs();
79     QCOMPARE(outputs.count(), 2);
80     QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
81     QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
82     Test::initWaylandWorkspace();
83 }
84 
init()85 void TestPointerConstraints::init()
86 {
87     QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::PointerConstraints));
88     QVERIFY(Test::waitForWaylandPointer());
89 
90     workspace()->setActiveOutput(QPoint(640, 512));
91     KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512));
92 }
93 
cleanup()94 void TestPointerConstraints::cleanup()
95 {
96     Test::destroyWaylandConnection();
97 }
98 
testConfinedPointer_data()99 void TestPointerConstraints::testConfinedPointer_data()
100 {
101     QTest::addColumn<PointerFunc>("positionFunction");
102     QTest::addColumn<int>("xOffset");
103     QTest::addColumn<int>("yOffset");
104     PointerFunc bottomLeft = &QRect::bottomLeft;
105     PointerFunc bottomRight = &QRect::bottomRight;
106     PointerFunc topRight = &QRect::topRight;
107     PointerFunc topLeft = &QRect::topLeft;
108 
109     QTest::newRow("XdgWmBase - bottomLeft")   << bottomLeft  << -1 << 1;
110     QTest::newRow("XdgWmBase - bottomRight")  << bottomRight << 1  << 1;
111     QTest::newRow("XdgWmBase - topLeft")      << topLeft  << -1 << -1;
112     QTest::newRow("XdgWmBase - topRight")     << topRight << 1  << -1;
113 }
114 
testConfinedPointer()115 void TestPointerConstraints::testConfinedPointer()
116 {
117     // this test sets up a Surface with a confined pointer
118     // simple interaction test to verify that the pointer gets confined
119     QScopedPointer<KWayland::Client::Surface> surface(Test::createSurface());
120     QScopedPointer<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.data()));
121     QScopedPointer<Pointer> pointer(Test::waylandSeat()->createPointer());
122     QScopedPointer<ConfinedPointer> confinedPointer(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot));
123     QSignalSpy confinedSpy(confinedPointer.data(), &ConfinedPointer::confined);
124     QVERIFY(confinedSpy.isValid());
125     QSignalSpy unconfinedSpy(confinedPointer.data(), &ConfinedPointer::unconfined);
126     QVERIFY(unconfinedSpy.isValid());
127 
128     // now map the window
129     auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue);
130     QVERIFY(c);
131     if (c->pos() == QPoint(0, 0)) {
132         c->move(QPoint(1, 1));
133     }
134     QVERIFY(!c->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos()));
135 
136     // now let's confine
137     QCOMPARE(input()->pointer()->isConstrained(), false);
138     KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center());
139     QCOMPARE(input()->pointer()->isConstrained(), true);
140     QVERIFY(confinedSpy.wait());
141 
142     // picking a position outside the window geometry should not move pointer
143     QSignalSpy pointerPositionChangedSpy(input(), &InputRedirection::globalPointerChanged);
144     QVERIFY(pointerPositionChangedSpy.isValid());
145     KWin::Cursors::self()->mouse()->setPos(QPoint(512, 512));
146     QVERIFY(pointerPositionChangedSpy.isEmpty());
147     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center());
148 
149     // TODO: test relative motion
150     QFETCH(PointerFunc, positionFunction);
151     const QPoint position = positionFunction(c->frameGeometry());
152     KWin::Cursors::self()->mouse()->setPos(position);
153     QCOMPARE(pointerPositionChangedSpy.count(), 1);
154     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
155     // moving one to right should not be possible
156     QFETCH(int, xOffset);
157     KWin::Cursors::self()->mouse()->setPos(position + QPoint(xOffset, 0));
158     QCOMPARE(pointerPositionChangedSpy.count(), 1);
159     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
160     // moving one to bottom should not be possible
161     QFETCH(int, yOffset);
162     KWin::Cursors::self()->mouse()->setPos(position + QPoint(0, yOffset));
163     QCOMPARE(pointerPositionChangedSpy.count(), 1);
164     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
165 
166     // modifier + click should be ignored
167     // first ensure the settings are ok
168     KConfigGroup group = kwinApp()->config()->group("MouseBindings");
169     group.writeEntry("CommandAllKey", QStringLiteral("Meta"));
170     group.writeEntry("CommandAll1", "Move");
171     group.writeEntry("CommandAll2", "Move");
172     group.writeEntry("CommandAll3", "Move");
173     group.writeEntry("CommandAllWheel", "change opacity");
174     group.sync();
175     workspace()->slotReconfigure();
176     QCOMPARE(options->commandAllModifier(), Qt::MetaModifier);
177     QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove);
178     QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove);
179     QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove);
180 
181     quint32 timestamp = 1;
182     kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++);
183     kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++);
184     QVERIFY(!c->isInteractiveMove());
185     kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++);
186 
187     // set the opacity to 0.5
188     c->setOpacity(0.5);
189     QCOMPARE(c->opacity(), 0.5);
190 
191     // pointer is confined so shortcut should not work
192     kwinApp()->platform()->pointerAxisVertical(-5, timestamp++);
193     QCOMPARE(c->opacity(), 0.5);
194     kwinApp()->platform()->pointerAxisVertical(5, timestamp++);
195     QCOMPARE(c->opacity(), 0.5);
196 
197     kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++);
198 
199     // deactivate the client, this should unconfine
200     workspace()->activateClient(nullptr);
201     QVERIFY(unconfinedSpy.wait());
202     QCOMPARE(input()->pointer()->isConstrained(), false);
203 
204     // reconfine pointer (this time with persistent life time)
205     confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent));
206     QSignalSpy confinedSpy2(confinedPointer.data(), &ConfinedPointer::confined);
207     QVERIFY(confinedSpy2.isValid());
208     QSignalSpy unconfinedSpy2(confinedPointer.data(), &ConfinedPointer::unconfined);
209     QVERIFY(unconfinedSpy2.isValid());
210 
211     // activate it again, this confines again
212     workspace()->activateClient(static_cast<AbstractClient*>(input()->pointer()->focus()));
213     QVERIFY(confinedSpy2.wait());
214     QCOMPARE(input()->pointer()->isConstrained(), true);
215 
216     // deactivate the client one more time with the persistent life time constraint, this should unconfine
217     workspace()->activateClient(nullptr);
218     QVERIFY(unconfinedSpy2.wait());
219     QCOMPARE(input()->pointer()->isConstrained(), false);
220     // activate it again, this confines again
221     workspace()->activateClient(static_cast<AbstractClient*>(input()->pointer()->focus()));
222     QVERIFY(confinedSpy2.wait());
223     QCOMPARE(input()->pointer()->isConstrained(), true);
224 
225     // create a second window and move it above our constrained window
226     QScopedPointer<KWayland::Client::Surface> surface2(Test::createSurface());
227     QScopedPointer<Test::XdgToplevel> shellSurface2(Test::createXdgToplevelSurface(surface2.data()));
228     auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(1280, 1024), Qt::blue);
229     QVERIFY(c2);
230     QVERIFY(unconfinedSpy2.wait());
231     // and unmapping the second window should confine again
232     shellSurface2.reset();
233     surface2.reset();
234     QVERIFY(confinedSpy2.wait());
235 
236     // let's set a region which results in unconfined
237     auto r = Test::waylandCompositor()->createRegion(QRegion(2, 2, 3, 3));
238     confinedPointer->setRegion(r.get());
239     surface->commit(KWayland::Client::Surface::CommitFlag::None);
240     QVERIFY(unconfinedSpy2.wait());
241     QCOMPARE(input()->pointer()->isConstrained(), false);
242     // and set a full region again, that should confine
243     confinedPointer->setRegion(nullptr);
244     surface->commit(KWayland::Client::Surface::CommitFlag::None);
245     QVERIFY(confinedSpy2.wait());
246     QCOMPARE(input()->pointer()->isConstrained(), true);
247 
248     // delete pointer confine
249     confinedPointer.reset(nullptr);
250     Test::flushWaylandConnection();
251 
252     QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWaylandServer::SurfaceInterface::pointerConstraintsChanged);
253     QVERIFY(constraintsChangedSpy.isValid());
254     QVERIFY(constraintsChangedSpy.wait());
255 
256     // should be unconfined
257     QCOMPARE(input()->pointer()->isConstrained(), false);
258 
259     // confine again
260     confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent));
261     QSignalSpy confinedSpy3(confinedPointer.data(), &ConfinedPointer::confined);
262     QVERIFY(confinedSpy3.isValid());
263     QVERIFY(confinedSpy3.wait());
264     QCOMPARE(input()->pointer()->isConstrained(), true);
265 
266     // and now unmap
267     shellSurface.reset();
268     surface.reset();
269     QVERIFY(Test::waitForWindowDestroyed(c));
270     QCOMPARE(input()->pointer()->isConstrained(), false);
271 }
272 
testLockedPointer()273 void TestPointerConstraints::testLockedPointer()
274 {
275     // this test sets up a Surface with a locked pointer
276     // simple interaction test to verify that the pointer gets locked
277     // the various ways to unlock are not tested as that's already verified by testConfinedPointer
278     QScopedPointer<KWayland::Client::Surface> surface(Test::createSurface());
279     QScopedPointer<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.data()));
280     QScopedPointer<Pointer> pointer(Test::waylandSeat()->createPointer());
281     QScopedPointer<LockedPointer> lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot));
282     QSignalSpy lockedSpy(lockedPointer.data(), &LockedPointer::locked);
283     QVERIFY(lockedSpy.isValid());
284     QSignalSpy unlockedSpy(lockedPointer.data(), &LockedPointer::unlocked);
285     QVERIFY(unlockedSpy.isValid());
286 
287     // now map the window
288     auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue);
289     QVERIFY(c);
290     QVERIFY(!c->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos()));
291 
292     // now let's lock
293     QCOMPARE(input()->pointer()->isConstrained(), false);
294     KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center());
295     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center());
296     QCOMPARE(input()->pointer()->isConstrained(), true);
297     QVERIFY(lockedSpy.wait());
298 
299     // try to move the pointer
300     // TODO: add relative pointer
301     KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center() + QPoint(1, 1));
302     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center());
303 
304     // deactivate the client, this should unlock
305     workspace()->activateClient(nullptr);
306     QCOMPARE(input()->pointer()->isConstrained(), false);
307     QVERIFY(unlockedSpy.wait());
308 
309     // moving cursor should be allowed again
310     KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center() + QPoint(1, 1));
311     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center() + QPoint(1, 1));
312 
313     lockedPointer.reset(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent));
314     QSignalSpy lockedSpy2(lockedPointer.data(), &LockedPointer::locked);
315     QVERIFY(lockedSpy2.isValid());
316 
317     // activate the client again, this should lock again
318     workspace()->activateClient(static_cast<AbstractClient*>(input()->pointer()->focus()));
319     QVERIFY(lockedSpy2.wait());
320     QCOMPARE(input()->pointer()->isConstrained(), true);
321 
322     // try to move the pointer
323     QCOMPARE(input()->pointer()->isConstrained(), true);
324     KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center());
325     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center() + QPoint(1, 1));
326 
327     // delete pointer lock
328     lockedPointer.reset(nullptr);
329     Test::flushWaylandConnection();
330 
331     QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &KWaylandServer::SurfaceInterface::pointerConstraintsChanged);
332     QVERIFY(constraintsChangedSpy.isValid());
333     QVERIFY(constraintsChangedSpy.wait());
334 
335     // moving cursor should be allowed again
336     QCOMPARE(input()->pointer()->isConstrained(), false);
337     KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center());
338     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center());
339 }
340 
testCloseWindowWithLockedPointer()341 void TestPointerConstraints::testCloseWindowWithLockedPointer()
342 {
343     // test case which verifies that the pointer gets unlocked when the window for it gets closed
344     QScopedPointer<KWayland::Client::Surface> surface(Test::createSurface());
345     QScopedPointer<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.data()));
346     QScopedPointer<Pointer> pointer(Test::waylandSeat()->createPointer());
347     QScopedPointer<LockedPointer> lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot));
348     QSignalSpy lockedSpy(lockedPointer.data(), &LockedPointer::locked);
349     QVERIFY(lockedSpy.isValid());
350     QSignalSpy unlockedSpy(lockedPointer.data(), &LockedPointer::unlocked);
351     QVERIFY(unlockedSpy.isValid());
352 
353     // now map the window
354     auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue);
355     QVERIFY(c);
356     QVERIFY(!c->frameGeometry().contains(KWin::Cursors::self()->mouse()->pos()));
357 
358     // now let's lock
359     QCOMPARE(input()->pointer()->isConstrained(), false);
360     KWin::Cursors::self()->mouse()->setPos(c->frameGeometry().center());
361     QCOMPARE(KWin::Cursors::self()->mouse()->pos(), c->frameGeometry().center());
362     QCOMPARE(input()->pointer()->isConstrained(), true);
363     QVERIFY(lockedSpy.wait());
364 
365     // close the window
366     shellSurface.reset();
367     surface.reset();
368     // this should result in unlocked
369     QVERIFY(unlockedSpy.wait());
370     QCOMPARE(input()->pointer()->isConstrained(), false);
371 }
372 
373 WAYLANDTEST_MAIN(TestPointerConstraints)
374 #include "pointer_constraints_test.moc"
375