You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
389 lines
12 KiB
389 lines
12 KiB
/* |
|
SPDX-FileCopyrightText: 2018 Roman Gilg <subdiff@gmail.com> |
|
|
|
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
|
*/ |
|
#include "pointerconstraintstest.h" |
|
|
|
#include <KWayland/Client/compositor.h> |
|
#include <KWayland/Client/connection_thread.h> |
|
#include <KWayland/Client/pointer.h> |
|
#include <KWayland/Client/pointerconstraints.h> |
|
#include <KWayland/Client/region.h> |
|
#include <KWayland/Client/registry.h> |
|
#include <KWayland/Client/seat.h> |
|
#include <KWayland/Client/surface.h> |
|
|
|
#include <QCursor> |
|
#include <QGuiApplication> |
|
#include <QQmlContext> |
|
#include <QQmlEngine> |
|
|
|
#include <QDebug> |
|
|
|
#include <xcb/xproto.h> |
|
|
|
using namespace KWayland::Client; |
|
|
|
WaylandBackend::WaylandBackend(QObject *parent) |
|
: Backend(parent) |
|
, m_connectionThreadObject(ConnectionThread::fromApplication(this)) |
|
{ |
|
setMode(Mode::Wayland); |
|
} |
|
|
|
void WaylandBackend::init(QQuickView *view) |
|
{ |
|
Backend::init(view); |
|
|
|
Registry *registry = new Registry(this); |
|
setupRegistry(registry); |
|
} |
|
|
|
void WaylandBackend::setupRegistry(Registry *registry) |
|
{ |
|
connect(registry, &Registry::compositorAnnounced, this, |
|
[this, registry](quint32 name, quint32 version) { |
|
m_compositor = registry->createCompositor(name, version, this); |
|
}); |
|
connect(registry, &Registry::seatAnnounced, this, |
|
[this, registry](quint32 name, quint32 version) { |
|
m_seat = registry->createSeat(name, version, this); |
|
if (m_seat->hasPointer()) { |
|
m_pointer = m_seat->createPointer(this); |
|
} |
|
connect(m_seat, &Seat::hasPointerChanged, this, |
|
[this]() { |
|
delete m_pointer; |
|
m_pointer = m_seat->createPointer(this); |
|
}); |
|
}); |
|
connect(registry, &Registry::pointerConstraintsUnstableV1Announced, this, |
|
[this, registry](quint32 name, quint32 version) { |
|
m_pointerConstraints = registry->createPointerConstraints(name, version, this); |
|
}); |
|
connect(registry, &Registry::interfacesAnnounced, this, |
|
[this] { |
|
Q_ASSERT(m_compositor); |
|
Q_ASSERT(m_seat); |
|
Q_ASSERT(m_pointerConstraints); |
|
}); |
|
registry->create(m_connectionThreadObject); |
|
registry->setup(); |
|
} |
|
|
|
bool WaylandBackend::isLocked() |
|
{ |
|
return m_lockedPointer && m_lockedPointer->isValid(); |
|
} |
|
|
|
bool WaylandBackend::isConfined() |
|
{ |
|
return m_confinedPointer && m_confinedPointer->isValid(); |
|
} |
|
|
|
static PointerConstraints::LifeTime lifeTime(bool persistent) |
|
{ |
|
return persistent ? PointerConstraints::LifeTime::Persistent : PointerConstraints::LifeTime::OneShot; |
|
} |
|
|
|
void WaylandBackend::lockRequest(bool persistent, QRect region) |
|
{ |
|
if (isLocked()) { |
|
if (!errorsAllowed()) { |
|
qDebug() << "Abort locking because already locked. Allow errors to test relocking (and crashing)."; |
|
return; |
|
} |
|
qDebug() << "Trying to lock although already locked. Crash expected."; |
|
} |
|
if (isConfined()) { |
|
if (!errorsAllowed()) { |
|
qDebug() << "Abort locking because already confined. Allow errors to test locking while being confined (and crashing)."; |
|
return; |
|
} |
|
qDebug() << "Trying to lock although already confined. Crash expected."; |
|
} |
|
qDebug() << "------ Lock requested ------"; |
|
qDebug() << "Persistent:" << persistent << "| Region:" << region; |
|
std::unique_ptr<Surface> winSurface(Surface::fromWindow(view())); |
|
std::unique_ptr<Region> wlRegion(m_compositor->createRegion(this)); |
|
wlRegion->add(region); |
|
|
|
auto *lockedPointer = m_pointerConstraints->lockPointer(winSurface.get(), |
|
m_pointer, |
|
wlRegion.get(), |
|
lifeTime(persistent), |
|
this); |
|
|
|
if (!lockedPointer) { |
|
qDebug() << "ERROR when receiving locked pointer!"; |
|
return; |
|
} |
|
m_lockedPointer = lockedPointer; |
|
m_lockedPointerPersistent = persistent; |
|
|
|
connect(lockedPointer, &LockedPointer::locked, this, [this]() { |
|
qDebug() << "------ LOCKED! ------"; |
|
if (lockHint()) { |
|
m_lockedPointer->setCursorPositionHint(QPointF(10., 10.)); |
|
Q_EMIT forceSurfaceCommit(); |
|
} |
|
|
|
Q_EMIT lockChanged(true); |
|
}); |
|
connect(lockedPointer, &LockedPointer::unlocked, this, [this]() { |
|
qDebug() << "------ UNLOCKED! ------"; |
|
if (!m_lockedPointerPersistent) { |
|
cleanupLock(); |
|
} |
|
Q_EMIT lockChanged(false); |
|
}); |
|
} |
|
|
|
void WaylandBackend::unlockRequest() |
|
{ |
|
if (!m_lockedPointer) { |
|
qDebug() << "Unlock requested, but there is no lock. Abort."; |
|
return; |
|
} |
|
qDebug() << "------ Unlock requested ------"; |
|
cleanupLock(); |
|
Q_EMIT lockChanged(false); |
|
} |
|
void WaylandBackend::cleanupLock() |
|
{ |
|
if (!m_lockedPointer) { |
|
return; |
|
} |
|
m_lockedPointer->release(); |
|
m_lockedPointer->deleteLater(); |
|
m_lockedPointer = nullptr; |
|
} |
|
|
|
void WaylandBackend::confineRequest(bool persistent, QRect region) |
|
{ |
|
if (isConfined()) { |
|
if (!errorsAllowed()) { |
|
qDebug() << "Abort confining because already confined. Allow errors to test reconfining (and crashing)."; |
|
return; |
|
} |
|
qDebug() << "Trying to lock although already locked. Crash expected."; |
|
} |
|
if (isLocked()) { |
|
if (!errorsAllowed()) { |
|
qDebug() << "Abort confining because already locked. Allow errors to test confining while being locked (and crashing)."; |
|
return; |
|
} |
|
qDebug() << "Trying to confine although already locked. Crash expected."; |
|
} |
|
qDebug() << "------ Confine requested ------"; |
|
qDebug() << "Persistent:" << persistent << "| Region:" << region; |
|
std::unique_ptr<Surface> winSurface(Surface::fromWindow(view())); |
|
std::unique_ptr<Region> wlRegion(m_compositor->createRegion(this)); |
|
wlRegion->add(region); |
|
|
|
auto *confinedPointer = m_pointerConstraints->confinePointer(winSurface.get(), |
|
m_pointer, |
|
wlRegion.get(), |
|
lifeTime(persistent), |
|
this); |
|
|
|
if (!confinedPointer) { |
|
qDebug() << "ERROR when receiving confined pointer!"; |
|
return; |
|
} |
|
m_confinedPointer = confinedPointer; |
|
m_confinedPointerPersistent = persistent; |
|
connect(confinedPointer, &ConfinedPointer::confined, this, [this]() { |
|
qDebug() << "------ CONFINED! ------"; |
|
Q_EMIT confineChanged(true); |
|
}); |
|
connect(confinedPointer, &ConfinedPointer::unconfined, this, [this]() { |
|
qDebug() << "------ UNCONFINED! ------"; |
|
if (!m_confinedPointerPersistent) { |
|
cleanupConfine(); |
|
} |
|
Q_EMIT confineChanged(false); |
|
}); |
|
} |
|
void WaylandBackend::unconfineRequest() |
|
{ |
|
if (!m_confinedPointer) { |
|
qDebug() << "Unconfine requested, but there is no confine. Abort."; |
|
return; |
|
} |
|
qDebug() << "------ Unconfine requested ------"; |
|
cleanupConfine(); |
|
Q_EMIT confineChanged(false); |
|
} |
|
void WaylandBackend::cleanupConfine() |
|
{ |
|
if (!m_confinedPointer) { |
|
return; |
|
} |
|
m_confinedPointer->release(); |
|
m_confinedPointer->deleteLater(); |
|
m_confinedPointer = nullptr; |
|
} |
|
|
|
XBackend::XBackend(QObject *parent) |
|
: Backend(parent) |
|
{ |
|
setMode(Mode::X); |
|
if (m_xcbConn) { |
|
xcb_disconnect(m_xcbConn); |
|
free(m_xcbConn); |
|
} |
|
} |
|
|
|
void XBackend::init(QQuickView *view) |
|
{ |
|
Backend::init(view); |
|
m_xcbConn = xcb_connect(nullptr, nullptr); |
|
if (!m_xcbConn) { |
|
qDebug() << "Could not open XCB connection."; |
|
} |
|
} |
|
|
|
void XBackend::lockRequest(bool persistent, QRect region) |
|
{ |
|
auto winId = view()->winId(); |
|
|
|
/* Cursor needs to be hidden such that Xwayland emulates warps. */ |
|
QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); |
|
|
|
auto cookie = xcb_warp_pointer_checked(m_xcbConn, /* connection */ |
|
XCB_NONE, /* src_w */ |
|
winId, /* dest_w */ |
|
0, /* src_x */ |
|
0, /* src_y */ |
|
0, /* src_width */ |
|
0, /* src_height */ |
|
20, /* dest_x */ |
|
20 /* dest_y */ |
|
); |
|
xcb_flush(m_xcbConn); |
|
|
|
xcb_generic_error_t *error = xcb_request_check(m_xcbConn, cookie); |
|
if (error) { |
|
qDebug() << "Lock (warp) failed with XCB error:" << error->error_code; |
|
free(error); |
|
return; |
|
} |
|
qDebug() << "LOCK (warp)"; |
|
Q_EMIT lockChanged(true); |
|
} |
|
|
|
void XBackend::unlockRequest() |
|
{ |
|
/* Xwayland unlocks the pointer, when the cursor is shown again. */ |
|
QGuiApplication::restoreOverrideCursor(); |
|
qDebug() << "------ Unlock requested ------"; |
|
Q_EMIT lockChanged(false); |
|
} |
|
|
|
void XBackend::confineRequest(bool persistent, QRect region) |
|
{ |
|
int error; |
|
if (!tryConfine(error)) { |
|
qDebug() << "Confine (grab) failed with XCB error:" << error; |
|
return; |
|
} |
|
qDebug() << "CONFINE (grab)"; |
|
Q_EMIT confineChanged(true); |
|
} |
|
|
|
void XBackend::unconfineRequest() |
|
{ |
|
auto cookie = xcb_ungrab_pointer_checked(m_xcbConn, XCB_CURRENT_TIME); |
|
xcb_flush(m_xcbConn); |
|
|
|
xcb_generic_error_t *error = xcb_request_check(m_xcbConn, cookie); |
|
if (error) { |
|
qDebug() << "Unconfine failed with XCB error:" << error->error_code; |
|
free(error); |
|
return; |
|
} |
|
qDebug() << "UNCONFINE (ungrab)"; |
|
Q_EMIT confineChanged(false); |
|
} |
|
|
|
void XBackend::hideAndConfineRequest(bool confineBeforeHide) |
|
{ |
|
if (!confineBeforeHide) { |
|
QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); |
|
} |
|
|
|
int error; |
|
if (!tryConfine(error)) { |
|
qDebug() << "Confine failed with XCB error:" << error; |
|
if (!confineBeforeHide) { |
|
QGuiApplication::restoreOverrideCursor(); |
|
} |
|
return; |
|
} |
|
if (confineBeforeHide) { |
|
QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); |
|
} |
|
qDebug() << "HIDE AND CONFINE (lock)"; |
|
Q_EMIT confineChanged(true); |
|
} |
|
|
|
void XBackend::undoHideRequest() |
|
{ |
|
QGuiApplication::restoreOverrideCursor(); |
|
qDebug() << "UNDO HIDE AND CONFINE (unlock)"; |
|
} |
|
|
|
bool XBackend::tryConfine(int &error) |
|
{ |
|
auto winId = view()->winId(); |
|
|
|
auto cookie = xcb_grab_pointer(m_xcbConn, /* display */ |
|
1, /* owner_events */ |
|
winId, /* grab_window */ |
|
0, /* event_mask */ |
|
XCB_GRAB_MODE_ASYNC, /* pointer_mode */ |
|
XCB_GRAB_MODE_ASYNC, /* keyboard_mode */ |
|
winId, /* confine_to */ |
|
XCB_NONE, /* cursor */ |
|
XCB_CURRENT_TIME /* time */ |
|
); |
|
xcb_flush(m_xcbConn); |
|
|
|
xcb_generic_error_t *e = nullptr; |
|
auto *reply = xcb_grab_pointer_reply(m_xcbConn, cookie, &e); |
|
if (!reply) { |
|
error = e->error_code; |
|
free(e); |
|
return false; |
|
} |
|
free(reply); |
|
return true; |
|
} |
|
|
|
int main(int argc, char **argv) |
|
{ |
|
QGuiApplication app(argc, argv); |
|
|
|
Backend *backend; |
|
if (app.platformName() == QStringLiteral("wayland")) { |
|
qDebug() << "Starting up: Wayland native mode"; |
|
backend = new WaylandBackend(&app); |
|
} else { |
|
qDebug() << "Starting up: Xserver/Xwayland legacy mode"; |
|
backend = new XBackend(&app); |
|
} |
|
|
|
QQuickView view; |
|
|
|
QQmlContext *context = view.engine()->rootContext(); |
|
context->setContextProperty(QStringLiteral("org_kde_kwin_tests_pointerconstraints_backend"), backend); |
|
|
|
view.setSource(QUrl::fromLocalFile(QStringLiteral(DIR) + QStringLiteral("/pointerconstraintstest.qml"))); |
|
view.show(); |
|
|
|
backend->init(&view); |
|
|
|
return app.exec(); |
|
}
|
|
|