diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -54,6 +54,10 @@ add_executable(dpmsTest dpmstest.cpp) target_link_libraries(dpmsTest KF5::WaylandClient Qt5::Widgets) ecm_mark_as_test(dpmsTest) + + add_executable(pointerConstraintsTest pointerconstraints.cpp) + target_link_libraries(pointerConstraintsTest KF5::WaylandClient Qt5::Widgets) + ecm_mark_as_test(pointerConstraintsTest) endif() add_executable(plasmasurface-test plasmasurfacetest.cpp) diff --git a/tests/pointerconstraints.h b/tests/pointerconstraints.h new file mode 100644 --- /dev/null +++ b/tests/pointerconstraints.h @@ -0,0 +1,174 @@ +/******************************************************************** +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 + +namespace KWayland { +namespace Client { + +class ConnectionThread; +class Registry; +class Compositor; +class Seat; +class Pointer; +class PointerConstraints; +class LockedPointer; +class ConfinedPointer; + +} +} + +class QCheckBox; +class QPushButton; +class QKeyEvent; +class QLabel; + +class MainWindow; + +class Backend : public QObject +{ + Q_OBJECT +public: + Backend(QObject *parent = nullptr) : QObject(parent) {} + + virtual void init(MainWindow *window) { + m_mainWindow = window; + } + + virtual void lockRequest(bool persistent = true, QRect region = QRect()) = 0; + virtual void unlockRequest() = 0; + + virtual void confineRequest(bool persistent = true, QRect region = QRect()) = 0; + virtual void unconfineRequest() = 0; + +Q_SIGNALS: + void confineChanged(bool confined); + void lockChanged(bool locked); + +protected: + MainWindow *m_mainWindow; +}; + +class WaylandBackend : public Backend +{ + Q_OBJECT +public: + WaylandBackend(QObject *parent = nullptr); + + void init(MainWindow *window) 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); + + 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 TestArea : public QFrame +{ + Q_OBJECT +public: + explicit TestArea(QWidget *parent = nullptr); + void setUsed(bool use); +Q_SIGNALS: + void unconfine(); +private: + QLabel *m_label; +}; + +class Controls : public QWidget +{ + Q_OBJECT +public: + explicit Controls(QWidget *parent = nullptr); + + bool lockPersistent() const; + bool confinePersistent() const; + + bool restrictInputArea() const; + bool wholeWindow() const; + bool errorsAllowed() const; + + void enableLockButton(bool enable); + void enableConfineButton(bool enable); + +Q_SIGNALS: + void lock(); + void confine(); + void wholeWindowChanged(bool whole); + +private: + bool m_restrictInputArea = false; + bool m_wholeWindow = false; + + QPushButton *m_lockButton; + QPushButton *m_confineButton; + + QCheckBox *m_lockPersistentCheckbox; + QCheckBox *m_confinePersistentCheckbox; + + QCheckBox *m_restrictInputAreaCheckbox; + QCheckBox *m_wholeWindowCheckbox; + QCheckBox *m_errorsAllowedCheckbox; +}; + +class MainWindow : public QWidget +{ + Q_OBJECT +public: + explicit MainWindow(Backend *backend, QWidget *parent = nullptr); + virtual ~MainWindow() = default; + + void init(); + + void keyPressEvent(QKeyEvent *event) override; + + bool errorsAllowed() const; + +private: + QRect regionRect() const; + + Backend *m_backend; + Controls *m_controls; + TestArea *m_testAreaInInput; + +}; + +#endif diff --git a/tests/pointerconstraints.cpp b/tests/pointerconstraints.cpp new file mode 100644 --- /dev/null +++ b/tests/pointerconstraints.cpp @@ -0,0 +1,402 @@ +/******************************************************************** +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 "pointerconstraints.h" + +#include "../src/client/compositor.h" +#include "../src/client/connection_thread.h" +#include "../src/client/registry.h" +#include "../src/client/surface.h" +#include "../src/client/region.h" +#include "../src/client/seat.h" +#include "../src/client/pointer.h" +#include "../src/client/pointerconstraints.h" + +#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)) +{ +} + +void WaylandBackend::init(MainWindow *window) { + Backend::init(window); + + 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(); +} + +static PointerConstraints::LifeTime lifeTime(bool persistent) +{ + return persistent ? PointerConstraints::LifeTime::Persistent : + PointerConstraints::LifeTime::OneShot; +} + +void WaylandBackend::lockRequest(bool persistent, QRect region) +{ + if (m_lockedPointer && m_lockedPointer->isValid() && !m_mainWindow->errorsAllowed()) { + qDebug() << "Abort locking because already locked. Allow errors to test relocking (and crashing)."; + return; + } + qDebug() << "------ Lock requested ------"; + qDebug() << "Persistent:" << persistent << "| Region:" << region; + QScopedPointer winSurface(Surface::fromWindow(m_mainWindow->windowHandle())); + 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) { + 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); + }); + } else { + qDebug() << "ERROR when receiving locked pointer!"; + } + +} +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 (m_confinedPointer && m_confinedPointer->isValid() && !m_mainWindow->errorsAllowed()) { + qDebug() << "Abort confining because already confined. Abort confining. Allow errors to test reconfining (and crashing)."; + return; + } + qDebug() << "------ Confine requested ------"; + qDebug() << "Persistent:" << persistent << "| Region:" << region; + QScopedPointer winSurface(Surface::fromWindow(m_mainWindow->windowHandle())); + 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) { + 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); + }); + } else { + qDebug() << "ERROR when receiving confined pointer!"; + } +} +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; +} + +TestArea::TestArea(QWidget *parent) + : QFrame(parent) +{ + setFixedSize(QSize(400, 200)); + setLineWidth(2); + setFrameStyle(QFrame::Box | QFrame::Plain); + + QPushButton *unconfineButton = new QPushButton(QLatin1String("Unconfine cursor"), this); + unconfineButton->move(0.5 * (width() - unconfineButton->width()), 0.5 * (height() - unconfineButton->height())); + connect(unconfineButton, &QPushButton::clicked, this, &TestArea::unconfine); + + m_label = new QLabel(QLatin1String("Activation area"), this); + m_label->move(0.5 * (width() - m_label->width()), 0.02 * height()); +} + +void TestArea::setUsed(bool use) +{ + m_label->setEnabled(use); +} + +Controls::Controls(QWidget *parent) + : QWidget(parent) +{ + QGridLayout *ctrlsLayout = new QGridLayout(this); + + m_lockButton = new QPushButton(QLatin1String("Lock cursor"), this); + m_confineButton = new QPushButton(QLatin1String("Confine cursor"), this); + + m_lockPersistentCheckbox = new QCheckBox(QLatin1String("Persistent lock"), this); + m_confinePersistentCheckbox = new QCheckBox(QLatin1String("Persistent confine"), this); + m_lockPersistentCheckbox->setChecked(true); + m_confinePersistentCheckbox->setChecked(true); + + m_restrictInputAreaCheckbox = new QCheckBox(QLatin1String("Restrict input area (not yet implemented)"), this); + m_restrictInputAreaCheckbox->setEnabled(false); + m_wholeWindowCheckbox = new QCheckBox(QLatin1String("Full window area activates"), this); + m_errorsAllowedCheckbox = new QCheckBox(QLatin1String("Allow critical errors"), this); + + ctrlsLayout->addWidget(m_lockButton, 0, 0); + ctrlsLayout->addWidget(m_lockPersistentCheckbox, 0, 1); + + ctrlsLayout->addWidget(m_confineButton, 1, 0); + ctrlsLayout->addWidget(m_confinePersistentCheckbox, 1, 1); + + ctrlsLayout->addWidget(m_restrictInputAreaCheckbox, 2, 0, 2, 2); + ctrlsLayout->addWidget(m_wholeWindowCheckbox, 3, 0, 3, 2); + + ctrlsLayout->addWidget(m_errorsAllowedCheckbox, 4, 0, 4, 2); + + setLayout(ctrlsLayout); + + connect(m_lockButton, &QPushButton::clicked, this, &Controls::lock); + connect(m_confineButton, &QPushButton::clicked, this, &Controls::confine); + + connect(m_wholeWindowCheckbox, &QCheckBox::stateChanged, this, [this](int state) { + Q_EMIT wholeWindowChanged(state == Qt::Checked ? true : false); + }); +} + +bool Controls::lockPersistent() const +{ + return m_lockPersistentCheckbox->isChecked(); +} +bool Controls::confinePersistent() const +{ + return m_confinePersistentCheckbox->isChecked(); +} +bool Controls::restrictInputArea() const +{ + return m_restrictInputAreaCheckbox->isChecked(); +} +bool Controls::wholeWindow() const +{ + return m_wholeWindowCheckbox->isChecked(); +} +bool Controls::errorsAllowed() const +{ + return m_errorsAllowedCheckbox->isChecked(); +} + +void Controls::enableLockButton(bool enable) +{ + m_lockButton->setEnabled(enable); +} +void Controls::enableConfineButton(bool enable) +{ + m_confineButton->setEnabled(enable); +} + +MainWindow::MainWindow(Backend *backend, QWidget *parent) + : QWidget(parent), + m_backend(backend) +{ + QVBoxLayout *mainLayout = new QVBoxLayout(this); + + m_controls = new Controls(this); + m_testAreaInInput = new TestArea(this); + + mainLayout->addWidget(m_controls); + mainLayout->addWidget(m_testAreaInInput); + + mainLayout->addWidget(new QLabel(QLatin1String("Lock: L / Unlock: K\n" + "Confine: C / Unconfine: X"), + this)); + setLayout(mainLayout); +} + +QRect MainWindow::regionRect() const +{ + if (m_controls->wholeWindow()) { + return QRect(); + } else { + return QRect(m_testAreaInInput->mapTo(this, QPoint(0,0)), m_testAreaInInput->size()); + } +} + +void MainWindow::init() +{ + connect(m_controls, &Controls::lock, this, [this]() { + m_backend->lockRequest(m_controls->lockPersistent(), regionRect()); + }); + connect(m_controls, &Controls::confine, this, [this]() { + m_backend->confineRequest(m_controls->confinePersistent(), regionRect()); + }); + connect(m_controls, &Controls::wholeWindowChanged, this, [this](bool whole) { + m_testAreaInInput->setUsed(!whole); + }); + + connect(m_testAreaInInput, &TestArea::unconfine, m_backend, &Backend::unconfineRequest); + + connect(m_backend, &Backend::lockChanged, this, [this](bool locked) { + locked = m_controls->errorsAllowed() ? false : locked; + m_controls->enableLockButton(!locked); + }); + connect(m_backend, &Backend::confineChanged, this, [this](bool confined) { + confined = m_controls->errorsAllowed() ? false : confined; + m_controls->enableConfineButton(!confined); + }); +} + +void MainWindow::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_L) { + m_backend->lockRequest(m_controls->lockPersistent(), regionRect()); + return; + } + if (event->key() == Qt::Key_K) { + m_backend->unlockRequest(); + return; + } + if (event->key() == Qt::Key_C) { + m_backend->confineRequest(m_controls->confinePersistent(), regionRect()); + return; + } + if (event->key() == Qt::Key_X) { + m_backend->unconfineRequest(); + return; + } + QWidget::keyPressEvent(event); +} + +bool MainWindow::errorsAllowed() const +{ + return m_controls->errorsAllowed(); +} + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + + Backend *backend; + if (app.platformName() == QLatin1String("wayland")) { + qDebug() << "Starting up: Wayland native mode"; + backend = new WaylandBackend(&app); + } else { + qDebug() << "Starting up: Xserver/Xwayland legacy mode"; + qDebug() << "Not yet implemented! Exit..."; + return -1; + } + + MainWindow window(backend); + window.show(); + + backend->init(&window); + window.init(); + + return app.exec(); +} + +#include "pointerconstraints.moc"