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