diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -35,6 +35,10 @@ add_executable(x11shadowreader x11shadowreader.cpp) target_link_libraries(x11shadowreader XCB::XCB Qt5::Widgets Qt5::X11Extras KF5::ConfigCore KF5::WindowSystem) +add_executable(pointerconstraints pointerconstraintstest.cpp) +add_definitions(-DDIR="${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(pointerconstraints XCB::XCB Qt5::Gui Qt5::Quick KF5::WaylandClient) + add_executable(pointergestures pointergesturestest.cpp) add_definitions(-DDIR="${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(pointergestures Qt5::Gui Qt5::Quick KF5::WaylandClient) diff --git a/tests/pointerconstraintstest.h b/tests/pointerconstraintstest.h new file mode 100644 --- /dev/null +++ b/tests/pointerconstraintstest.h @@ -0,0 +1,168 @@ +/******************************************************************** +Copyright 2018 Roman Gilg + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) version 3, or any +later version accepted by the membership of KDE e.V. (or its +successor approved by the membership of KDE e.V.), which shall +act as a proxy defined in Section 6 of version 3 of the license. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ +#ifndef POINTERCONSTRAINTSTEST_H +#define POINTERCONSTRAINTSTEST_H + +#include +#include + +#include + +namespace KWayland { +namespace Client { + +class ConnectionThread; +class Registry; +class Compositor; +class Seat; +class Pointer; +class PointerConstraints; +class LockedPointer; +class ConfinedPointer; + +} +} + +class MainWindow; + +class Backend : public QObject +{ + Q_OBJECT +public: + Backend(QObject *parent = nullptr) : QObject(parent) {} + + Q_PROPERTY(int mode READ mode CONSTANT) + Q_PROPERTY(bool errorsAllowed READ errorsAllowed WRITE setErrorsAllowed NOTIFY errorsAllowedChanged) + + virtual void init(QQuickView *view) { + m_view = view; + } + int mode() const { + return (int)m_mode; + } + bool errorsAllowed() const { + return m_errorsAllowed; + } + void setErrorsAllowed(bool set) { + if (m_errorsAllowed == set) { + return; + } + m_errorsAllowed = set; + Q_EMIT errorsAllowedChanged(); + } + + Q_INVOKABLE virtual void lockRequest(bool persistent = true, QRect region = QRect()) { + Q_UNUSED(persistent); + Q_UNUSED(region); + } + Q_INVOKABLE virtual void unlockRequest() {} + + Q_INVOKABLE virtual void confineRequest(bool persistent = true, QRect region = QRect()) { + Q_UNUSED(persistent); + Q_UNUSED(region); + } + Q_INVOKABLE virtual void unconfineRequest() {} + Q_INVOKABLE virtual void hideAndConfineRequest(bool confineBeforeHide = false) { + Q_UNUSED(confineBeforeHide); + } + Q_INVOKABLE virtual void undoHideRequest() {} + +Q_SIGNALS: + void confineChanged(bool confined); + void lockChanged(bool locked); + void errorsAllowedChanged(); + +protected: + enum class Mode { + Wayland = 0, + X = 1 + }; + + QQuickView* view() const { + return m_view; + } + void setMode(Mode set) { + m_mode = set; + } + +private: + QQuickView *m_view; + Mode m_mode; + bool m_errorsAllowed = false; +}; + +class WaylandBackend : public Backend +{ + Q_OBJECT +public: + WaylandBackend(QObject *parent = nullptr); + + void init(QQuickView *view) override; + + void lockRequest(bool persistent, QRect region) override; + void unlockRequest() override; + + void confineRequest(bool persistent, QRect region) override; + void unconfineRequest() override; + +private: + void setupRegistry(KWayland::Client::Registry *registry); + + bool isLocked(); + bool isConfined(); + + void cleanupLock(); + void cleanupConfine(); + + KWayland::Client::ConnectionThread *m_connectionThreadObject; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::Seat *m_seat = nullptr; + KWayland::Client::Pointer *m_pointer = nullptr; + KWayland::Client::PointerConstraints *m_pointerConstraints = nullptr; + + KWayland::Client::LockedPointer *m_lockedPointer = nullptr; + bool m_lockedPointerPersistent = false; + KWayland::Client::ConfinedPointer *m_confinedPointer = nullptr; + bool m_confinedPointerPersistent = false; +}; + +class XBackend : public Backend +{ +public: + XBackend(QObject *parent = nullptr); + + void init(QQuickView *view) override; + + void lockRequest(bool persistent, QRect region) override; + void unlockRequest() override; + + void confineRequest(bool persistent, QRect region) override; + void unconfineRequest() override; + + void hideAndConfineRequest(bool confineBeforeHide) override; + void undoHideRequest() override; + +private: + bool tryConfine(int &error); + xcb_connection_t *m_xcbConn = nullptr; + +}; + +#endif diff --git a/tests/pointerconstraintstest.cpp b/tests/pointerconstraintstest.cpp new file mode 100644 --- /dev/null +++ b/tests/pointerconstraintstest.cpp @@ -0,0 +1,411 @@ +/******************************************************************** +Copyright 2018 Roman Gilg + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) version 3, or any +later version accepted by the membership of KDE e.V. (or its +successor approved by the membership of KDE e.V.), which shall +act as a proxy defined in Section 6 of version 3 of the license. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ +#include "pointerconstraintstest.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include + +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; + QScopedPointer winSurface(Surface::fromWindow(view())); + QScopedPointer wlRegion(m_compositor->createRegion(this)); + wlRegion->add(region); + + auto *lockedPointer = m_pointerConstraints->lockPointer(winSurface.data(), + m_pointer, + wlRegion.data(), + 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! ------"; + 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; + QScopedPointer winSurface(Surface::fromWindow(view())); + QScopedPointer wlRegion(m_compositor->createRegion(this)); + wlRegion->add(region); + + auto *confinedPointer = m_pointerConstraints->confinePointer(winSurface.data(), + m_pointer, + wlRegion.data(), + 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(NULL, NULL); + if (!m_xcbConn) { + qDebug() << "Could not open XCB connection."; + } +} + +void XBackend::lockRequest(bool persistent, QRect region) +{ + Q_UNUSED(persistent); + Q_UNUSED(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) +{ + Q_UNUSED(persistent); + Q_UNUSED(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(); +} diff --git a/tests/pointerconstraintstest.qml b/tests/pointerconstraintstest.qml new file mode 100644 --- /dev/null +++ b/tests/pointerconstraintstest.qml @@ -0,0 +1,190 @@ +/******************************************************************** +Copyright 2018 Roman Gilg + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) version 3, or any +later version accepted by the membership of KDE e.V. (or its +successor approved by the membership of KDE e.V.), which shall +act as a proxy defined in Section 6 of version 3 of the license. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . +*********************************************************************/ +import QtQuick 2.10 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.1 + +ColumnLayout { +/* for margins */ +ColumnLayout { + id: root + focus: true + + Layout.margins: 20 + + function lock() { + org_kde_kwin_tests_pointerconstraints_backend.lockRequest(lockPersChck.checked, root.activRect()); + } + function confine() { + org_kde_kwin_tests_pointerconstraints_backend.confineRequest(confPersChck.checked, root.activRect()); + } + function unlock() { + org_kde_kwin_tests_pointerconstraints_backend.unlockRequest(); + } + function unconfine() { + org_kde_kwin_tests_pointerconstraints_backend.unconfineRequest(); + } + function hideAndConfine() { + org_kde_kwin_tests_pointerconstraints_backend.hideAndConfineRequest(); + } + function undoHideAndConfine() { + org_kde_kwin_tests_pointerconstraints_backend.undoHideRequest(); + } + + property bool waylandNative: org_kde_kwin_tests_pointerconstraints_backend.mode === 0 + + Keys.onPressed: { + if (event.key === Qt.Key_L) { + root.lock(); + event.accepted = true; + } else if (event.key === Qt.Key_C) { + root.confine(); + event.accepted = true; + } else if (event.key === Qt.Key_K) { + root.unlock(); + event.accepted = true; + } else if (event.key === Qt.Key_X) { + root.unconfine(); + event.accepted = true; + } else if (event.key === Qt.Key_H) { + root.hideAndConfine(); + event.accepted = true; + } else if (event.key === Qt.Key_G) { + root.undoHideAndConfine(); + event.accepted = true; + } + } + + function activRect() { + if (fullWindowChck.checked) { + return Qt.rect(0, 0, -1, -1); + } + return activArea.rect(); + } + + GridLayout { + columns: 2 + rowSpacing: 10 + columnSpacing: 10 + + Button { + id: lockButton + text: "Lock pointer" + onClicked: root.lock() + } + CheckBox { + id: lockPersChck + text: "Persistent lock" + checked: root.waylandNative + enabled: root.waylandNative + } + Button { + id: confButton + text: "Confine pointer" + onClicked: root.confine() + } + CheckBox { + id: confPersChck + text: "Persistent confine" + checked: root.waylandNative + enabled: root.waylandNative + } + Button { + id: hideConfButton + text: "Hide and confine pointer" + onClicked: root.hideAndConfine() + visible: !root.waylandNative + } + CheckBox { + id: confBeforeHideChck + text: "Confine first, then hide" + checked: false + visible: !root.waylandNative + } + } + + CheckBox { + id: restrAreaChck + text: "Restrict input area (not yet implemented)" + enabled: false + } + CheckBox { + id: fullWindowChck + text: "Full window area activates" + checked: !root.waylandNative + enabled: root.waylandNative + } + CheckBox { + id: errorsChck + text: "Allow critical errors" + checked: false + enabled: root.waylandNative + onCheckedChanged: org_kde_kwin_tests_pointerconstraints_backend.errorsAllowed = checked; + } + + Item { + width: childrenRect.width + height: childrenRect.height + + Rectangle { + id: activArea + + width: 400 + height: 200 + + enabled: root.waylandNative && !fullWindowChck.checked + + function rect() { + var globalPt = mapToGlobal(x, y); + return Qt.rect(globalPt.x, globalPt.y, width, height); + } + + border.color: enabled ? "black" : "lightgrey" + border.width: 2 + + Label { + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + text: "Activation area" + } + } + Button { + id: unconfButton + anchors.horizontalCenter: activArea.horizontalCenter + anchors.verticalCenter: activArea.verticalCenter + + text: "Unconfine pointer" + onClicked: root.unconfineRequest() + } + } + + Label { + text: "Lock: L / Unlock: K" + } + Label { + text: "Confine: C / Unconfine: X" + } + Label { + text: "Hide cursor and confine pointer: H / undo hide: G" + visible: !root.waylandNative + } +} + +}