1 /*
2 SPDX-FileCopyrightText: 2018 Roman Gilg <subdiff@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
5 */
6 #include "pointerconstraintstest.h"
7
8 #include <KWayland/Client/compositor.h>
9 #include <KWayland/Client/connection_thread.h>
10 #include <KWayland/Client/registry.h>
11 #include <KWayland/Client/surface.h>
12 #include <KWayland/Client/region.h>
13 #include <KWayland/Client/seat.h>
14 #include <KWayland/Client/pointer.h>
15 #include <KWayland/Client/pointerconstraints.h>
16
17 #include <QGuiApplication>
18 #include <QQmlContext>
19 #include <QQmlEngine>
20 #include <QCursor>
21
22 #include <QDebug>
23 #include <QScopedPointer>
24
25 #include <xcb/xproto.h>
26
27 using namespace KWayland::Client;
28
WaylandBackend(QObject * parent)29 WaylandBackend::WaylandBackend(QObject *parent)
30 : Backend(parent)
31 , m_connectionThreadObject(ConnectionThread::fromApplication(this))
32 {
33 setMode(Mode::Wayland);
34 }
35
init(QQuickView * view)36 void WaylandBackend::init(QQuickView *view)
37 {
38 Backend::init(view);
39
40 Registry *registry = new Registry(this);
41 setupRegistry(registry);
42 }
43
setupRegistry(Registry * registry)44 void WaylandBackend::setupRegistry(Registry *registry)
45 {
46 connect(registry, &Registry::compositorAnnounced, this,
47 [this, registry](quint32 name, quint32 version) {
48 m_compositor = registry->createCompositor(name, version, this);
49 }
50 );
51 connect(registry, &Registry::seatAnnounced, this,
52 [this, registry](quint32 name, quint32 version) {
53 m_seat = registry->createSeat(name, version, this);
54 if (m_seat->hasPointer()) {
55 m_pointer = m_seat->createPointer(this);
56 }
57 connect(m_seat, &Seat::hasPointerChanged, this,
58 [this]() {
59 delete m_pointer;
60 m_pointer = m_seat->createPointer(this);
61 }
62 );
63 }
64 );
65 connect(registry, &Registry::pointerConstraintsUnstableV1Announced, this,
66 [this, registry](quint32 name, quint32 version) {
67 m_pointerConstraints = registry->createPointerConstraints(name, version, this);
68 }
69 );
70 connect(registry, &Registry::interfacesAnnounced, this,
71 [this] {
72 Q_ASSERT(m_compositor);
73 Q_ASSERT(m_seat);
74 Q_ASSERT(m_pointerConstraints);
75 }
76 );
77 registry->create(m_connectionThreadObject);
78 registry->setup();
79 }
80
isLocked()81 bool WaylandBackend::isLocked()
82 {
83 return m_lockedPointer && m_lockedPointer->isValid();
84 }
85
isConfined()86 bool WaylandBackend::isConfined()
87 {
88 return m_confinedPointer && m_confinedPointer->isValid();
89 }
90
lifeTime(bool persistent)91 static PointerConstraints::LifeTime lifeTime(bool persistent)
92 {
93 return persistent ? PointerConstraints::LifeTime::Persistent :
94 PointerConstraints::LifeTime::OneShot;
95 }
96
lockRequest(bool persistent,QRect region)97 void WaylandBackend::lockRequest(bool persistent, QRect region)
98 {
99 if (isLocked()) {
100 if (!errorsAllowed()) {
101 qDebug() << "Abort locking because already locked. Allow errors to test relocking (and crashing).";
102 return;
103 }
104 qDebug() << "Trying to lock although already locked. Crash expected.";
105 }
106 if (isConfined()) {
107 if (!errorsAllowed()) {
108 qDebug() << "Abort locking because already confined. Allow errors to test locking while being confined (and crashing).";
109 return;
110 }
111 qDebug() << "Trying to lock although already confined. Crash expected.";
112 }
113 qDebug() << "------ Lock requested ------";
114 qDebug() << "Persistent:" << persistent << "| Region:" << region;
115 QScopedPointer<Surface> winSurface(Surface::fromWindow(view()));
116 QScopedPointer<Region> wlRegion(m_compositor->createRegion(this));
117 wlRegion->add(region);
118
119 auto *lockedPointer = m_pointerConstraints->lockPointer(winSurface.data(),
120 m_pointer,
121 wlRegion.data(),
122 lifeTime(persistent),
123 this);
124
125 if (!lockedPointer) {
126 qDebug() << "ERROR when receiving locked pointer!";
127 return;
128 }
129 m_lockedPointer = lockedPointer;
130 m_lockedPointerPersistent = persistent;
131
132 connect(lockedPointer, &LockedPointer::locked, this, [this]() {
133 qDebug() << "------ LOCKED! ------";
134 if(lockHint()) {
135 m_lockedPointer->setCursorPositionHint(QPointF(10., 10.));
136 Q_EMIT forceSurfaceCommit();
137 }
138
139 Q_EMIT lockChanged(true);
140 });
141 connect(lockedPointer, &LockedPointer::unlocked, this, [this]() {
142 qDebug() << "------ UNLOCKED! ------";
143 if (!m_lockedPointerPersistent) {
144 cleanupLock();
145 }
146 Q_EMIT lockChanged(false);
147 });
148 }
149
unlockRequest()150 void WaylandBackend::unlockRequest()
151 {
152 if (!m_lockedPointer) {
153 qDebug() << "Unlock requested, but there is no lock. Abort.";
154 return;
155 }
156 qDebug() << "------ Unlock requested ------";
157 cleanupLock();
158 Q_EMIT lockChanged(false);
159 }
cleanupLock()160 void WaylandBackend::cleanupLock()
161 {
162 if (!m_lockedPointer) {
163 return;
164 }
165 m_lockedPointer->release();
166 m_lockedPointer->deleteLater();
167 m_lockedPointer = nullptr;
168 }
169
confineRequest(bool persistent,QRect region)170 void WaylandBackend::confineRequest(bool persistent, QRect region)
171 {
172 if (isConfined()) {
173 if (!errorsAllowed()) {
174 qDebug() << "Abort confining because already confined. Allow errors to test reconfining (and crashing).";
175 return;
176 }
177 qDebug() << "Trying to lock although already locked. Crash expected.";
178 }
179 if (isLocked()) {
180 if (!errorsAllowed()) {
181 qDebug() << "Abort confining because already locked. Allow errors to test confining while being locked (and crashing).";
182 return;
183 }
184 qDebug() << "Trying to confine although already locked. Crash expected.";
185 }
186 qDebug() << "------ Confine requested ------";
187 qDebug() << "Persistent:" << persistent << "| Region:" << region;
188 QScopedPointer<Surface> winSurface(Surface::fromWindow(view()));
189 QScopedPointer<Region> wlRegion(m_compositor->createRegion(this));
190 wlRegion->add(region);
191
192 auto *confinedPointer = m_pointerConstraints->confinePointer(winSurface.data(),
193 m_pointer,
194 wlRegion.data(),
195 lifeTime(persistent),
196 this);
197
198 if (!confinedPointer) {
199 qDebug() << "ERROR when receiving confined pointer!";
200 return;
201 }
202 m_confinedPointer = confinedPointer;
203 m_confinedPointerPersistent = persistent;
204 connect(confinedPointer, &ConfinedPointer::confined, this, [this]() {
205 qDebug() << "------ CONFINED! ------";
206 Q_EMIT confineChanged(true);
207 });
208 connect(confinedPointer, &ConfinedPointer::unconfined, this, [this]() {
209 qDebug() << "------ UNCONFINED! ------";
210 if (!m_confinedPointerPersistent) {
211 cleanupConfine();
212 }
213 Q_EMIT confineChanged(false);
214 });
215 }
unconfineRequest()216 void WaylandBackend::unconfineRequest()
217 {
218 if (!m_confinedPointer) {
219 qDebug() << "Unconfine requested, but there is no confine. Abort.";
220 return;
221 }
222 qDebug() << "------ Unconfine requested ------";
223 cleanupConfine();
224 Q_EMIT confineChanged(false);
225 }
cleanupConfine()226 void WaylandBackend::cleanupConfine()
227 {
228 if (!m_confinedPointer) {
229 return;
230 }
231 m_confinedPointer->release();
232 m_confinedPointer->deleteLater();
233 m_confinedPointer = nullptr;
234 }
235
XBackend(QObject * parent)236 XBackend::XBackend(QObject *parent)
237 : Backend(parent)
238 {
239 setMode(Mode::X);
240 if (m_xcbConn) {
241 xcb_disconnect(m_xcbConn);
242 free(m_xcbConn);
243 }
244 }
245
init(QQuickView * view)246 void XBackend::init(QQuickView *view)
247 {
248 Backend::init(view);
249 m_xcbConn = xcb_connect(nullptr, nullptr);
250 if (!m_xcbConn) {
251 qDebug() << "Could not open XCB connection.";
252 }
253 }
254
lockRequest(bool persistent,QRect region)255 void XBackend::lockRequest(bool persistent, QRect region)
256 {
257 Q_UNUSED(persistent);
258 Q_UNUSED(region);
259
260 auto winId = view()->winId();
261
262 /* Cursor needs to be hidden such that Xwayland emulates warps. */
263 QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
264
265 auto cookie = xcb_warp_pointer_checked(m_xcbConn, /* connection */
266 XCB_NONE, /* src_w */
267 winId, /* dest_w */
268 0, /* src_x */
269 0, /* src_y */
270 0, /* src_width */
271 0, /* src_height */
272 20, /* dest_x */
273 20 /* dest_y */
274 );
275 xcb_flush(m_xcbConn);
276
277 xcb_generic_error_t *error = xcb_request_check(m_xcbConn, cookie);
278 if (error) {
279 qDebug() << "Lock (warp) failed with XCB error:" << error->error_code;
280 free(error);
281 return;
282 }
283 qDebug() << "LOCK (warp)";
284 Q_EMIT lockChanged(true);
285 }
286
unlockRequest()287 void XBackend::unlockRequest()
288 {
289 /* Xwayland unlocks the pointer, when the cursor is shown again. */
290 QGuiApplication::restoreOverrideCursor();
291 qDebug() << "------ Unlock requested ------";
292 Q_EMIT lockChanged(false);
293 }
294
confineRequest(bool persistent,QRect region)295 void XBackend::confineRequest(bool persistent, QRect region)
296 {
297 Q_UNUSED(persistent);
298 Q_UNUSED(region);
299
300 int error;
301 if (!tryConfine(error)) {
302 qDebug() << "Confine (grab) failed with XCB error:" << error;
303 return;
304 }
305 qDebug() << "CONFINE (grab)";
306 Q_EMIT confineChanged(true);
307 }
308
unconfineRequest()309 void XBackend::unconfineRequest()
310 {
311 auto cookie = xcb_ungrab_pointer_checked(m_xcbConn, XCB_CURRENT_TIME);
312 xcb_flush(m_xcbConn);
313
314 xcb_generic_error_t *error = xcb_request_check(m_xcbConn, cookie);
315 if (error) {
316 qDebug() << "Unconfine failed with XCB error:" << error->error_code;
317 free(error);
318 return;
319 }
320 qDebug() << "UNCONFINE (ungrab)";
321 Q_EMIT confineChanged(false);
322 }
323
hideAndConfineRequest(bool confineBeforeHide)324 void XBackend::hideAndConfineRequest(bool confineBeforeHide)
325 {
326 if (!confineBeforeHide) {
327 QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
328 }
329
330 int error;
331 if (!tryConfine(error)) {
332 qDebug() << "Confine failed with XCB error:" << error;
333 if (!confineBeforeHide) {
334 QGuiApplication::restoreOverrideCursor();
335 }
336 return;
337 }
338 if (confineBeforeHide) {
339 QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor));
340 }
341 qDebug() << "HIDE AND CONFINE (lock)";
342 Q_EMIT confineChanged(true);
343
344 }
345
undoHideRequest()346 void XBackend::undoHideRequest()
347 {
348 QGuiApplication::restoreOverrideCursor();
349 qDebug() << "UNDO HIDE AND CONFINE (unlock)";
350 }
351
tryConfine(int & error)352 bool XBackend::tryConfine(int &error)
353 {
354 auto winId = view()->winId();
355
356 auto cookie = xcb_grab_pointer(m_xcbConn, /* display */
357 1, /* owner_events */
358 winId, /* grab_window */
359 0, /* event_mask */
360 XCB_GRAB_MODE_ASYNC, /* pointer_mode */
361 XCB_GRAB_MODE_ASYNC, /* keyboard_mode */
362 winId, /* confine_to */
363 XCB_NONE, /* cursor */
364 XCB_CURRENT_TIME /* time */
365 );
366 xcb_flush(m_xcbConn);
367
368 xcb_generic_error_t *e = nullptr;
369 auto *reply = xcb_grab_pointer_reply(m_xcbConn, cookie, &e);
370 if (!reply) {
371 error = e->error_code;
372 free(e);
373 return false;
374 }
375 free(reply);
376 return true;
377 }
378
main(int argc,char ** argv)379 int main(int argc, char **argv)
380 {
381 QGuiApplication app(argc, argv);
382
383 Backend *backend;
384 if (app.platformName() == QStringLiteral("wayland")) {
385 qDebug() << "Starting up: Wayland native mode";
386 backend = new WaylandBackend(&app);
387 } else {
388 qDebug() << "Starting up: Xserver/Xwayland legacy mode";
389 backend = new XBackend(&app);
390 }
391
392 QQuickView view;
393
394 QQmlContext* context = view.engine()->rootContext();
395 context->setContextProperty(QStringLiteral("org_kde_kwin_tests_pointerconstraints_backend"), backend);
396
397 view.setSource(QUrl::fromLocalFile(QStringLiteral(DIR) +QStringLiteral("/pointerconstraintstest.qml")));
398 view.show();
399
400 backend->init(&view);
401
402 return app.exec();
403 }
404