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