KWin
Loading...
Searching...
No Matches
pointer_constraints_test.cpp
Go to the documentation of this file.
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
11#include "core/output.h"
12#include "cursor.h"
13#include "keyboard_input.h"
14#include "pointer_input.h"
15#include "wayland/seat.h"
16#include "wayland/surface.h"
17#include "wayland_server.h"
18#include "window.h"
19#include "workspace.h"
20
21#include <KWayland/Client/compositor.h>
22#include <KWayland/Client/keyboard.h>
23#include <KWayland/Client/pointer.h>
24#include <KWayland/Client/pointerconstraints.h>
25#include <KWayland/Client/region.h>
26#include <KWayland/Client/seat.h>
27#include <KWayland/Client/shm_pool.h>
28#include <KWayland/Client/surface.h>
29
30#include <linux/input.h>
31
32#include <functional>
33
34using namespace KWin;
35
36typedef std::function<QPointF(const QRectF &)> PointerFunc;
38
39static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_constraints-0");
40
41class TestPointerConstraints : public QObject
42{
43 Q_OBJECT
44private Q_SLOTS:
45 void initTestCase();
46 void init();
47 void cleanup();
48
49 void testConfinedPointer_data();
50 void testConfinedPointer();
51 void testLockedPointer();
52 void testCloseWindowWithLockedPointer();
53};
54
55void TestPointerConstraints::initTestCase()
56{
57 qRegisterMetaType<PointerFunc>();
58 qRegisterMetaType<KWin::Window *>();
59 QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
60 QVERIFY(waylandServer()->init(s_socketName));
62 QRect(0, 0, 1280, 1024),
63 QRect(1280, 0, 1280, 1024),
64 });
65
66 // set custom config which disables the OnScreenNotification
67 KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig);
68 KConfigGroup group = config->group(QStringLiteral("OnScreenNotification"));
69 group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml"));
70 group.sync();
71
72 kwinApp()->setConfig(config);
73
74 kwinApp()->start();
75 QVERIFY(applicationStartedSpy.wait());
76 const auto outputs = workspace()->outputs();
77 QCOMPARE(outputs.count(), 2);
78 QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
79 QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
80}
81
82void TestPointerConstraints::init()
83{
84 QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::PointerConstraints));
86
87 workspace()->setActiveOutput(QPoint(640, 512));
88 KWin::input()->pointer()->warp(QPoint(640, 512));
89}
90
91void TestPointerConstraints::cleanup()
92{
94}
95
96void TestPointerConstraints::testConfinedPointer_data()
97{
98 QTest::addColumn<PointerFunc>("positionFunction");
99 QTest::addColumn<int>("xOffset");
100 QTest::addColumn<int>("yOffset");
101 PointerFunc bottomLeft = [](const QRectF &rect) {
102 return rect.toRect().bottomLeft();
103 };
104 PointerFunc bottomRight = [](const QRectF &rect) {
105 return rect.toRect().bottomRight();
106 };
107 PointerFunc topRight = [](const QRectF &rect) {
108 return rect.toRect().topRight();
109 };
110 PointerFunc topLeft = [](const QRectF &rect) {
111 return rect.toRect().topLeft();
112 };
113
114 QTest::newRow("XdgWmBase - bottomLeft") << bottomLeft << -1 << 1;
115 QTest::newRow("XdgWmBase - bottomRight") << bottomRight << 1 << 1;
116 QTest::newRow("XdgWmBase - topLeft") << topLeft << -1 << -1;
117 QTest::newRow("XdgWmBase - topRight") << topRight << 1 << -1;
118}
119
120void TestPointerConstraints::testConfinedPointer()
121{
122 // this test sets up a Surface with a confined pointer
123 // simple interaction test to verify that the pointer gets confined
124 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
125 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
126 std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
127 std::unique_ptr<KWayland::Client::ConfinedPointer> confinedPointer(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot));
128 QSignalSpy confinedSpy(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined);
129 QSignalSpy unconfinedSpy(confinedPointer.get(), &KWayland::Client::ConfinedPointer::unconfined);
130
131 // now map the window
132 auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
133 QVERIFY(window);
134 if (window->pos() == QPoint(0, 0)) {
135 window->move(QPoint(1, 1));
136 }
137 QVERIFY(!exclusiveContains(window->frameGeometry(), KWin::Cursors::self()->mouse()->pos()));
138
139 // now let's confine
140 QCOMPARE(input()->pointer()->isConstrained(), false);
141 KWin::input()->pointer()->warp(window->frameGeometry().center());
142 QCOMPARE(input()->pointer()->isConstrained(), true);
143 QVERIFY(confinedSpy.wait());
144
145 // picking a position outside the window geometry should not move pointer
146 QSignalSpy pointerPositionChangedSpy(input(), &InputRedirection::globalPointerChanged);
147 KWin::input()->pointer()->warp(QPoint(512, 512));
148 QVERIFY(pointerPositionChangedSpy.isEmpty());
149 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
150
151 // TODO: test relative motion
152 QFETCH(PointerFunc, positionFunction);
153 const QPointF position = positionFunction(window->frameGeometry());
154 KWin::input()->pointer()->warp(position);
155 QCOMPARE(pointerPositionChangedSpy.count(), 1);
156 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
157 // moving one to right should not be possible
158 QFETCH(int, xOffset);
159 KWin::input()->pointer()->warp(position + QPoint(xOffset, 0));
160 QCOMPARE(pointerPositionChangedSpy.count(), 1);
161 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
162 // moving one to bottom should not be possible
163 QFETCH(int, yOffset);
164 KWin::input()->pointer()->warp(position + QPoint(0, yOffset));
165 QCOMPARE(pointerPositionChangedSpy.count(), 1);
166 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), position);
167
168 // modifier + click should be ignored
169 // first ensure the settings are ok
170 KConfigGroup group = kwinApp()->config()->group(QStringLiteral("MouseBindings"));
171 group.writeEntry("CommandAllKey", QStringLiteral("Meta"));
172 group.writeEntry("CommandAll1", "Move");
173 group.writeEntry("CommandAll2", "Move");
174 group.writeEntry("CommandAll3", "Move");
175 group.writeEntry("CommandAllWheel", "change opacity");
176 group.sync();
178 QCOMPARE(options->commandAllModifier(), Qt::MetaModifier);
182
183 quint32 timestamp = 1;
184 Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
185 Test::pointerButtonPressed(BTN_LEFT, timestamp++);
186 QVERIFY(!window->isInteractiveMove());
187 Test::pointerButtonReleased(BTN_LEFT, timestamp++);
188
189 // set the opacity to 0.5
190 window->setOpacity(0.5);
191 QCOMPARE(window->opacity(), 0.5);
192
193 // pointer is confined so shortcut should not work
194 Test::pointerAxisVertical(-5, timestamp++);
195 QCOMPARE(window->opacity(), 0.5);
196 Test::pointerAxisVertical(5, timestamp++);
197 QCOMPARE(window->opacity(), 0.5);
198
199 Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
200
201 // deactivate the window, this should unconfine
202 workspace()->activateWindow(nullptr);
203 QVERIFY(unconfinedSpy.wait());
204 QCOMPARE(input()->pointer()->isConstrained(), false);
205
206 // reconfine pointer (this time with persistent life time)
207 confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
208 QSignalSpy confinedSpy2(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined);
209 QSignalSpy unconfinedSpy2(confinedPointer.get(), &KWayland::Client::ConfinedPointer::unconfined);
210
211 // activate it again, this confines again
212 workspace()->activateWindow(static_cast<Window *>(input()->pointer()->focus()));
213 QVERIFY(confinedSpy2.wait());
214 QCOMPARE(input()->pointer()->isConstrained(), true);
215
216 // deactivate the window one more time with the persistent life time constraint, this should unconfine
217 workspace()->activateWindow(nullptr);
218 QVERIFY(unconfinedSpy2.wait());
219 QCOMPARE(input()->pointer()->isConstrained(), false);
220 // activate it again, this confines again
221 workspace()->activateWindow(static_cast<Window *>(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 std::unique_ptr<KWayland::Client::Surface> surface2(Test::createSurface());
227 std::unique_ptr<Test::XdgToplevel> shellSurface2(Test::createXdgToplevelSurface(surface2.get()));
228 auto c2 = Test::renderAndWaitForShown(surface2.get(), 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);
251
252 QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &SurfaceInterface::pointerConstraintsChanged);
253 QVERIFY(constraintsChangedSpy.wait());
254
255 // should be unconfined
256 QCOMPARE(input()->pointer()->isConstrained(), false);
257
258 // confine again
259 confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
260 QSignalSpy confinedSpy3(confinedPointer.get(), &KWayland::Client::ConfinedPointer::confined);
261 QVERIFY(confinedSpy3.wait());
262 QCOMPARE(input()->pointer()->isConstrained(), true);
263
264 // and now unmap
265 shellSurface.reset();
266 surface.reset();
267 QVERIFY(Test::waitForWindowClosed(window));
268 QCOMPARE(input()->pointer()->isConstrained(), false);
269}
270
271void TestPointerConstraints::testLockedPointer()
272{
273 // this test sets up a Surface with a locked pointer
274 // simple interaction test to verify that the pointer gets locked
275 // the various ways to unlock are not tested as that's already verified by testConfinedPointer
276 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
277 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
278 std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
279 std::unique_ptr<KWayland::Client::LockedPointer> lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot));
280 QSignalSpy lockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::locked);
281 QSignalSpy unlockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::unlocked);
282
283 // now map the window
284 auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
285 QVERIFY(window);
286 QVERIFY(!exclusiveContains(window->frameGeometry(), KWin::Cursors::self()->mouse()->pos()));
287
288 // now let's lock
289 QCOMPARE(input()->pointer()->isConstrained(), false);
290 KWin::input()->pointer()->warp(window->frameGeometry().center());
291 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
292 QCOMPARE(input()->pointer()->isConstrained(), true);
293 QVERIFY(lockedSpy.wait());
294
295 // try to move the pointer
296 // TODO: add relative pointer
297 KWin::input()->pointer()->warp(window->frameGeometry().center() + QPoint(1, 1));
298 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
299
300 // deactivate the window, this should unlock
301 workspace()->activateWindow(nullptr);
302 QCOMPARE(input()->pointer()->isConstrained(), false);
303 QVERIFY(unlockedSpy.wait());
304
305 // moving cursor should be allowed again
306 KWin::input()->pointer()->warp(window->frameGeometry().center() + QPoint(1, 1));
307 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1));
308
309 lockedPointer.reset(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::Persistent));
310 QSignalSpy lockedSpy2(lockedPointer.get(), &KWayland::Client::LockedPointer::locked);
311
312 // activate the window again, this should lock again
313 workspace()->activateWindow(static_cast<Window *>(input()->pointer()->focus()));
314 QVERIFY(lockedSpy2.wait());
315 QCOMPARE(input()->pointer()->isConstrained(), true);
316
317 // try to move the pointer
318 QCOMPARE(input()->pointer()->isConstrained(), true);
319 KWin::input()->pointer()->warp(window->frameGeometry().center());
320 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center() + QPoint(1, 1));
321
322 // delete pointer lock
323 lockedPointer.reset(nullptr);
325
326 QSignalSpy constraintsChangedSpy(input()->pointer()->focus()->surface(), &SurfaceInterface::pointerConstraintsChanged);
327 QVERIFY(constraintsChangedSpy.wait());
328
329 // moving cursor should be allowed again
330 QCOMPARE(input()->pointer()->isConstrained(), false);
331 KWin::input()->pointer()->warp(window->frameGeometry().center());
332 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
333}
334
335void TestPointerConstraints::testCloseWindowWithLockedPointer()
336{
337 // test case which verifies that the pointer gets unlocked when the window for it gets closed
338 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
339 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
340 std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
341 std::unique_ptr<KWayland::Client::LockedPointer> lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.get(), pointer.get(), nullptr, KWayland::Client::PointerConstraints::LifeTime::OneShot));
342 QSignalSpy lockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::locked);
343 QSignalSpy unlockedSpy(lockedPointer.get(), &KWayland::Client::LockedPointer::unlocked);
344
345 // now map the window
346 auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
347 QVERIFY(window);
348 QVERIFY(!exclusiveContains(window->frameGeometry(), KWin::Cursors::self()->mouse()->pos()));
349
350 // now let's lock
351 QCOMPARE(input()->pointer()->isConstrained(), false);
352 KWin::input()->pointer()->warp(window->frameGeometry().center());
353 QCOMPARE(KWin::Cursors::self()->mouse()->pos(), window->frameGeometry().center());
354 QCOMPARE(input()->pointer()->isConstrained(), true);
355 QVERIFY(lockedSpy.wait());
356
357 // close the window
358 shellSurface.reset();
359 surface.reset();
360 // this should result in unlocked
361 QVERIFY(unlockedSpy.wait());
362 QCOMPARE(input()->pointer()->isConstrained(), false);
363}
364
366#include "pointer_constraints_test.moc"
QPointF pos()
Definition cursor.cpp:204
static Cursors * self()
Definition cursor.cpp:35
Cursor * mouse() const
Definition cursor.h:266
void globalPointerChanged(const QPointF &pos)
Emitted when the global pointer position changed.
PointerInputRedirection * pointer() const
Definition input.h:220
@ MouseUnrestrictedMove
Definition options.h:456
Qt::KeyboardModifier commandAllModifier() const
Definition options.h:558
MouseCommand commandAll2
Definition options.h:153
MouseCommand commandAll3
Definition options.h:154
MouseCommand commandAll1
Definition options.h:152
void warp(const QPointF &pos)
void activateWindow(Window *window, bool force=false)
QList< Output * > outputs() const
Definition workspace.h:762
void setActiveOutput(Output *output)
void slotReconfigure()
Q_DECLARE_METATYPE(KWin::SwitchEvent::State)
#define WAYLANDTEST_MAIN(TestObject)
Window * renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format=QImage::Format_ARGB32, int timeout=5000)
void keyboardKeyReleased(quint32 key, quint32 time)
void destroyWaylandConnection()
void setOutputConfig(const QList< QRect > &geometries)
void pointerAxisVertical(qreal delta, quint32 time, qint32 discreteDelta=0, InputRedirection::PointerAxisSource source=InputRedirection::PointerAxisSourceUnknown)
void keyboardKeyPressed(quint32 key, quint32 time)
bool setupWaylandConnection(AdditionalWaylandInterfaces flags=AdditionalWaylandInterfaces())
KWayland::Client::Compositor * waylandCompositor()
KWayland::Client::PointerConstraints * waylandPointerConstraints()
KWayland::Client::Seat * waylandSeat()
void pointerButtonPressed(quint32 button, quint32 time)
QList< KWayland::Client::Output * > outputs
std::unique_ptr< KWayland::Client::Surface > createSurface()
XdgToplevel * createXdgToplevelSurface(KWayland::Client::Surface *surface, QObject *parent=nullptr)
bool waitForWaylandPointer()
void pointerButtonReleased(quint32 button, quint32 time)
void flushWaylandConnection()
bool waitForWindowClosed(Window *window)
WaylandServer * waylandServer()
Workspace * workspace()
Definition workspace.h:830
Options * options
Definition main.cpp:73
InputRedirection * input()
Definition input.h:549
std::function< QPointF(const QRectF &) PointerFunc)