diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt index 8dcd4c8e06..6134011749 100644 --- a/autotests/integration/CMakeLists.txt +++ b/autotests/integration/CMakeLists.txt @@ -42,6 +42,7 @@ integrationTest(NAME testModiferOnlyShortcut SRCS modifier_only_shortcut_test.cp integrationTest(NAME testTabBox SRCS tabbox_test.cpp) integrationTest(NAME testGlobalShortcuts SRCS globalshortcuts_test.cpp) integrationTest(NAME testWindowSelection SRCS window_selection_test.cpp) +integrationTest(NAME testPointerConstraints SRCS pointer_constraints_test.cpp) if (XCB_ICCCM_FOUND) integrationTest(NAME testMoveResize SRCS move_resize_window_test.cpp LIBS XCB::ICCCM) diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h index be50cb4daa..2d68ae44c6 100644 --- a/autotests/integration/kwin_wayland_test.h +++ b/autotests/integration/kwin_wayland_test.h @@ -33,6 +33,7 @@ class ConnectionThread; class Compositor; class PlasmaShell; class PlasmaWindowManagement; +class PointerConstraints; class Seat; class ServerSideDecorationManager; class Shell; @@ -78,7 +79,8 @@ enum class AdditionalWaylandInterface { Seat = 1 << 0, Decoration = 1 << 1, PlasmaShell = 1 << 2, - WindowManagement = 1 << 3 + WindowManagement = 1 << 3, + PointerConstraints = 1 << 4 }; Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface) /** @@ -106,6 +108,7 @@ KWayland::Client::Seat *waylandSeat(); KWayland::Client::ServerSideDecorationManager *waylandServerSideDecoration(); KWayland::Client::PlasmaShell *waylandPlasmaShell(); KWayland::Client::PlasmaWindowManagement *waylandWindowManagement(); +KWayland::Client::PointerConstraints *waylandPointerConstraints(); bool waitForWaylandPointer(); bool waitForWaylandTouch(); diff --git a/autotests/integration/pointer_constraints_test.cpp b/autotests/integration/pointer_constraints_test.cpp new file mode 100644 index 0000000000..547572da57 --- /dev/null +++ b/autotests/integration/pointer_constraints_test.cpp @@ -0,0 +1,379 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "platform.h" +#include "pointer_input.h" +#include "shell_client.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using namespace KWin; +using namespace KWayland::Client; + +typedef std::function PointerFunc; +Q_DECLARE_METATYPE(PointerFunc) + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_pointer_constraints-0"); + +class TestPointerConstraints : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testConfinedPointer_data(); + void testConfinedPointer(); + void testLockedPointer_data(); + void testLockedPointer(); + void testBreakConstrainedPointer_data(); + void testBreakConstrainedPointer(); +}; + +void TestPointerConstraints::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setOutputCount", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + QCOMPARE(screens()->count(), 2); + QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); + QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); + waylandServer()->initWorkspace(); +} + +void TestPointerConstraints::init() +{ + QVERIFY(Test::setupWaylandConnection(s_socketName, Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::PointerConstraints)); + QVERIFY(Test::waitForWaylandPointer()); + + screens()->setCurrent(0); + KWin::Cursor::setPos(QPoint(1280, 512)); +} + +void TestPointerConstraints::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestPointerConstraints::testConfinedPointer_data() +{ + QTest::addColumn("type"); + QTest::addColumn("positionFunction"); + QTest::addColumn("xOffset"); + QTest::addColumn("yOffset"); + PointerFunc bottomLeft = &QRect::bottomLeft; + PointerFunc bottomRight = &QRect::bottomRight; + PointerFunc topRight = &QRect::topRight; + PointerFunc topLeft = &QRect::topLeft; + + QTest::newRow("wlShell - bottomLeft") << Test::ShellSurfaceType::WlShell << bottomLeft << -1 << 1; + QTest::newRow("wlShell - bottomRight") << Test::ShellSurfaceType::WlShell << bottomRight << 1 << 1; + QTest::newRow("wlShell - topLeft") << Test::ShellSurfaceType::WlShell << topLeft << -1 << -1; + QTest::newRow("wlShell - topRight") << Test::ShellSurfaceType::WlShell << topRight << 1 << -1; + QTest::newRow("XdgShellV5 - bottomLeft") << Test::ShellSurfaceType::XdgShellV5 << bottomLeft << -1 << 1; + QTest::newRow("XdgShellV5 - bottomRight") << Test::ShellSurfaceType::XdgShellV5 << bottomRight << 1 << 1; + QTest::newRow("XdgShellV5 - topLeft") << Test::ShellSurfaceType::XdgShellV5 << topLeft << -1 << -1; + QTest::newRow("XdgShellV5 - topRight") << Test::ShellSurfaceType::XdgShellV5 << topRight << 1 << -1; +} + +void TestPointerConstraints::testConfinedPointer() +{ + // this test sets up a Surface with a confined pointer + // simple interaction test to verify that the pointer gets confined + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer confinedPointer(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot)); + QSignalSpy confinedSpy(confinedPointer.data(), &ConfinedPointer::confined); + QVERIFY(confinedSpy.isValid()); + QSignalSpy unconfinedSpy(confinedPointer.data(), &ConfinedPointer::unconfined); + QVERIFY(unconfinedSpy.isValid()); + + // now map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c); + if (c->geometry().topLeft() == QPoint(0, 0)) { + c->move(QPoint(1, 1)); + } + QVERIFY(!c->geometry().contains(KWin::Cursor::pos())); + + // now let's confine + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(confinedSpy.wait()); + + // picking a position outside the window geometry should not move pointer + QSignalSpy pointerPositionChangedSpy(input(), &InputRedirection::globalPointerChanged); + QVERIFY(pointerPositionChangedSpy.isValid()); + KWin::Cursor::setPos(QPoint(1280, 512)); + QVERIFY(pointerPositionChangedSpy.isEmpty()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); + + // TODO: test relative motion + QFETCH(PointerFunc, positionFunction); + const QPoint position = positionFunction(c->geometry()); + KWin::Cursor::setPos(position); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursor::pos(), position); + // moving one to right should not be possible + QFETCH(int, xOffset); + KWin::Cursor::setPos(position + QPoint(xOffset, 0)); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursor::pos(), position); + // moving one to bottom should not be possible + QFETCH(int, yOffset); + KWin::Cursor::setPos(position + QPoint(0, yOffset)); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursor::pos(), position); + + // let's break the constraint explicitly + input()->pointer()->breakPointerConstraints(); + QCOMPARE(input()->pointer()->isConstrained(), false); + QVERIFY(unconfinedSpy.wait()); + confinedPointer.reset(Test::waylandPointerConstraints()->confinePointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent)); + QSignalSpy confinedSpy2(confinedPointer.data(), &ConfinedPointer::confined); + QVERIFY(confinedSpy2.isValid()); + QSignalSpy unconfinedSpy2(confinedPointer.data(), &ConfinedPointer::unconfined); + QVERIFY(unconfinedSpy2.isValid()); + // should get confined + QVERIFY(confinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // now let's unconfine again, any pointer movement should confine again + input()->pointer()->breakPointerConstraints(); + QCOMPARE(input()->pointer()->isConstrained(), false); + QVERIFY(unconfinedSpy2.wait()); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(confinedSpy2.wait()); + + // let's use the other break constraint and block + input()->pointer()->breakPointerConstraints(); + input()->pointer()->blockPointerConstraints(); + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursor::setPos(c->geometry().center() + QPoint(1, 1)); + QCOMPARE(input()->pointer()->isConstrained(), false); + QVERIFY(!confinedSpy2.wait()); + + // now move outside and back in again, that should confine + KWin::Cursor::setPos(c->geometry().bottomRight() + QPoint(1, 1)); + KWin::Cursor::setPos(c->geometry().center() + QPoint(1, 1)); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(confinedSpy2.wait()); + + // create a second window and move it above our constrained window + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(type, surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(c2); + QVERIFY(unconfinedSpy2.wait()); + // and unmapping the second window should confine again + shellSurface2.reset(); + surface2.reset(); + QVERIFY(confinedSpy2.wait()); + + // let's set a region which results in unconfined + auto r = Test::waylandCompositor()->createRegion(QRegion(0, 0, 1, 1)); + confinedPointer->setRegion(r.get()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(unconfinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), false); + // and set a full region again, that should confine + confinedPointer->setRegion(nullptr); + surface->commit(Surface::CommitFlag::None); + QVERIFY(confinedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // and now unmap + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(c)); + QCOMPARE(input()->pointer()->isConstrained(), false); +} + +void TestPointerConstraints::testLockedPointer_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; +} + +void TestPointerConstraints::testLockedPointer() +{ + // this test sets up a Surface with a locked pointer + // simple interaction test to verify that the pointer gets locked + // the various ways to unlock are not tested as that's already verified by testConfinedPointer + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::OneShot)); + QSignalSpy lockedSpy(lockedPointer.data(), &LockedPointer::locked); + QVERIFY(lockedSpy.isValid()); + QSignalSpy unlockedSpy(lockedPointer.data(), &LockedPointer::unlocked); + QVERIFY(unlockedSpy.isValid()); + + // now map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c); + QVERIFY(!c->geometry().contains(KWin::Cursor::pos())); + + // now let's lock + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(lockedSpy.wait()); + + // try to move the pointer + // TODO: add relative pointer + KWin::Cursor::setPos(c->geometry().center() + QPoint(1, 1)); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); + + // now unlock again + input()->pointer()->breakPointerConstraints(); + QCOMPARE(input()->pointer()->isConstrained(), false); + QVERIFY(unlockedSpy.wait()); + + // moving cursor should be allowed again + KWin::Cursor::setPos(c->geometry().center() + QPoint(1, 1)); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center() + QPoint(1, 1)); +} + +void TestPointerConstraints::testBreakConstrainedPointer_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; +} + +void TestPointerConstraints::testBreakConstrainedPointer() +{ + // this test verifies the breaking of Pointer constraints through the keyboard event filter + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + QScopedPointer pointer(Test::waylandSeat()->createPointer()); + QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); + QSignalSpy keyboardEnteredSpy(keyboard.data(), &Keyboard::entered); + QVERIFY(keyboardEnteredSpy.isValid()); + QSignalSpy keyboardLeftSpy(keyboard.data(), &Keyboard::left); + QVERIFY(keyboardLeftSpy.isValid()); + QSignalSpy keyChangedSpy(keyboard.data(), &Keyboard::keyChanged); + QVERIFY(keyChangedSpy.isValid()); + QScopedPointer lockedPointer(Test::waylandPointerConstraints()->lockPointer(surface.data(), pointer.data(), nullptr, PointerConstraints::LifeTime::Persistent)); + QSignalSpy lockedSpy(lockedPointer.data(), &LockedPointer::locked); + QVERIFY(lockedSpy.isValid()); + QSignalSpy unlockedSpy(lockedPointer.data(), &LockedPointer::unlocked); + QVERIFY(unlockedSpy.isValid()); + + // now map the window + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c); + QVERIFY(!c->geometry().contains(KWin::Cursor::pos())); + QVERIFY(keyboardEnteredSpy.wait()); + // now let's lock + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(lockedSpy.wait()); + + // now try to break + quint32 timestamp = 0; + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, timestamp++); + QVERIFY(keyboardLeftSpy.wait()); + // and just waiting should break constrain + QVERIFY(unlockedSpy.wait()); + QCOMPARE(input()->pointer()->isConstrained(), false); + // and should enter again + QTRY_COMPARE(keyboardEnteredSpy.count(), 2); + QCOMPARE(waylandServer()->seat()->focusedKeyboardSurface(), c->surface()); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); + QVERIFY(!keyChangedSpy.wait()); + QVERIFY(keyChangedSpy.isEmpty()); + + // now lock again + // need to move out and in + KWin::Cursor::setPos(c->geometry().bottomRight() + QPoint(1, 1)); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(lockedSpy.wait()); + + // and just do a key press/release on esc + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.last().at(0).value(), quint32(KEY_ESC)); + + // and another variant which won't break + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.last().at(0).value(), quint32(KEY_LEFTSHIFT)); + QVERIFY(!unlockedSpy.wait()); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); + QVERIFY(keyChangedSpy.wait()); + QCOMPARE(keyChangedSpy.last().at(0).value(), quint32(KEY_ESC)); + + // and now break for real + kwinApp()->platform()->keyboardKeyPressed(KEY_ESC, timestamp++); + QVERIFY(unlockedSpy.wait()); + kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++); +} + +WAYLANDTEST_MAIN(TestPointerConstraints) +#include "pointer_constraints_test.moc" diff --git a/autotests/integration/test_helpers.cpp b/autotests/integration/test_helpers.cpp index e30405a449..7d95b6cf41 100644 --- a/autotests/integration/test_helpers.cpp +++ b/autotests/integration/test_helpers.cpp @@ -28,6 +28,7 @@ along with this program. If not, see . #include #include #include +#include #include #include #include @@ -58,6 +59,7 @@ static struct { Seat *seat = nullptr; PlasmaShell *plasmaShell = nullptr; PlasmaWindowManagement *windowManagement = nullptr; + PointerConstraints *pointerConstraints = nullptr; QThread *thread = nullptr; } s_waylandConnection; @@ -147,6 +149,13 @@ bool setupWaylandConnection(const QString &socketName, AdditionalWaylandInterfac return false; } } + if (flags.testFlag(AdditionalWaylandInterface::PointerConstraints)) { + s_waylandConnection.pointerConstraints = registry.createPointerConstraints(registry.interface(Registry::Interface::PointerConstraintsUnstableV1).name, + registry.interface(Registry::Interface::PointerConstraintsUnstableV1).version); + if (!s_waylandConnection.pointerConstraints->isValid()) { + return false; + } + } return true; } @@ -165,6 +174,8 @@ void destroyWaylandConnection() s_waylandConnection.decoration = nullptr; delete s_waylandConnection.seat; s_waylandConnection.seat = nullptr; + delete s_waylandConnection.pointerConstraints; + s_waylandConnection.pointerConstraints = nullptr; delete s_waylandConnection.xdgShellV5; s_waylandConnection.xdgShellV5 = nullptr; delete s_waylandConnection.shell; @@ -227,6 +238,11 @@ PlasmaWindowManagement *waylandWindowManagement() return s_waylandConnection.windowManagement; } +PointerConstraints *waylandPointerConstraints() +{ + return s_waylandConnection.pointerConstraints; +} + bool waitForWaylandPointer() { if (!s_waylandConnection.seat) { diff --git a/input.cpp b/input.cpp index bdce5000b8..28ca85d2ff 100644 --- a/input.cpp +++ b/input.cpp @@ -394,6 +394,71 @@ private: } }; +class PointerConstraintsFilter : public InputEventFilter { +public: + explicit PointerConstraintsFilter() + : InputEventFilter() + , m_timer(new QTimer) + { + QObject::connect(m_timer.data(), &QTimer::timeout, + [this] { + input()->pointer()->breakPointerConstraints(); + input()->pointer()->blockPointerConstraints(); + // TODO: show notification + waylandServer()->seat()->keyReleased(m_keyCode); + cancel(); + } + ); + } + + bool keyEvent(QKeyEvent *event) override { + if (isActive()) { + if (event->type() == QEvent::KeyPress) { + // is that another key that gets pressed? + if (!event->isAutoRepeat() && event->key() != Qt::Key_Escape) { + cancel(); + return false; + } + if (event->isAutoRepeat() && event->key() == Qt::Key_Escape) { + // filter out + return true; + } + } else { + cancel(); + return false; + } + } else if (input()->pointer()->isConstrained()) { + if (event->type() == QEvent::KeyPress && + event->key() == Qt::Key_Escape && + input()->keyboard()->xkb()->modifiersRelevantForGlobalShortcuts() == Qt::KeyboardModifiers()) { + // TODO: don't hard code + m_timer->start(3000); + input()->keyboard()->update(); + m_keyCode = event->nativeScanCode(); + passToWaylandServer(event); + return true; + } + } + return false; + } + + void cancel() { + if (!isActive()) { + return; + } + m_timer->stop(); + input()->keyboard()->update(); + } + + bool isActive() const { + return m_timer->isActive(); + } + +private: + QScopedPointer m_timer; + int m_keyCode = 0; +}; + class EffectsFilter : public InputEventFilter { public: bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { @@ -1062,6 +1127,7 @@ public: if (event->buttons() == Qt::NoButton) { // update pointer window only if no button is pressed input()->pointer()->update(); + input()->pointer()->enablePointerConstraints(); } seat->setPointerPos(event->globalPos()); MouseEvent *e = static_cast(event); @@ -1424,6 +1490,8 @@ void InputRedirection::setupInputFilters() installInputEventFilter(new TerminateServerFilter); installInputEventFilter(new DragAndDropInputFilter); installInputEventFilter(new LockScreenFilter); + m_pointerConstraintsFilter = new PointerConstraintsFilter; + installInputEventFilter(m_pointerConstraintsFilter); m_windowSelector = new WindowSelectorFilter; installInputEventFilter(m_windowSelector); } @@ -1790,6 +1858,11 @@ bool InputRedirection::isSelectingWindow() const return m_windowSelector ? m_windowSelector->isActive() : false; } +bool InputRedirection::isBreakingPointerConstraints() const +{ + return m_pointerConstraintsFilter ? m_pointerConstraintsFilter->isActive() : false; +} + InputDeviceHandler::InputDeviceHandler(InputRedirection *input) : QObject(input) , m_input(input) diff --git a/input.h b/input.h index 5a33551a94..314f6e1381 100644 --- a/input.h +++ b/input.h @@ -42,6 +42,7 @@ class GlobalShortcutsManager; class Toplevel; class InputEventFilter; class KeyboardInputRedirection; +class PointerConstraintsFilter; class PointerInputRedirection; class TouchInputRedirection; class WindowSelectorFilter; @@ -173,6 +174,8 @@ public: void startInteractivePositionSelection(std::function callback); bool isSelectingWindow() const; + bool isBreakingPointerConstraints() const; + Q_SIGNALS: /** * @brief Emitted when the global pointer position changed @@ -231,6 +234,7 @@ private: LibInput::Connection *m_libInput = nullptr; WindowSelectorFilter *m_windowSelector = nullptr; + PointerConstraintsFilter *m_pointerConstraintsFilter = nullptr; QVector m_filters; KSharedConfigPtr m_inputConfig; diff --git a/keyboard_input.cpp b/keyboard_input.cpp index 439c624852..0ac9f6aca8 100644 --- a/keyboard_input.cpp +++ b/keyboard_input.cpp @@ -613,7 +613,7 @@ void KeyboardInputRedirection::update() break; } while (it != stacking.begin()); } - } else if (!input()->isSelectingWindow()) { + } else if (!input()->isSelectingWindow() && !input()->isBreakingPointerConstraints()) { found = workspace()->activeClient(); } if (found && found->surface()) { diff --git a/pointer_input.cpp b/pointer_input.cpp index 360caffb40..b7de7262aa 100644 --- a/pointer_input.cpp +++ b/pointer_input.cpp @@ -35,6 +35,7 @@ along with this program. If not, see . #include #include #include +#include #include #include // screenlocker @@ -177,6 +178,8 @@ void PointerInputRedirection::init() void PointerInputRedirection::updateOnStartMoveResize() { + breakPointerConstraints(m_window ? m_window->surface() : nullptr); + disconnectPointerConstraintsConnection(); m_window.clear(); waylandServer()->seat()->setFocusedPointerSurface(nullptr); } @@ -201,6 +204,8 @@ void PointerInputRedirection::updateToReset() } disconnect(m_windowGeometryConnection); m_windowGeometryConnection = QMetaObject::Connection(); + breakPointerConstraints(m_window->surface()); + disconnectPointerConstraintsConnection(); m_window.clear(); } waylandServer()->seat()->setFocusedPointerSurface(nullptr); @@ -451,6 +456,8 @@ void PointerInputRedirection::update() } disconnect(m_windowGeometryConnection); m_windowGeometryConnection = QMetaObject::Connection(); + breakPointerConstraints(oldWindow->surface()); + disconnectPointerConstraintsConnection(); } if (AbstractClient *c = qobject_cast(t)) { // only send enter if it wasn't on deco for the same client before @@ -483,6 +490,11 @@ void PointerInputRedirection::update() seat->setFocusedPointerSurfaceTransformation(m_window.data()->inputTransformation()); } ); + m_constraintsConnection = connect(m_window->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged, + this, &PointerInputRedirection::enablePointerConstraints); + // check whether a pointer confinement/lock fires + m_blockConstraint = false; + enablePointerConstraints(); } else { m_window.clear(); warpXcbOnSurfaceLeft(nullptr); @@ -491,6 +503,120 @@ void PointerInputRedirection::update() } } +void PointerInputRedirection::breakPointerConstraints(KWayland::Server::SurfaceInterface *surface) +{ + // cancel pointer constraints + if (surface) { + auto c = surface->confinedPointer(); + if (c && c->isConfined()) { + c->setConfined(false); + } + auto l = surface->lockedPointer(); + if (l && l->isLocked()) { + l->setLocked(false); + } + } + disconnectConfinedPointerRegionConnection(); + m_confined = false; + m_locked = false; +} + +void PointerInputRedirection::breakPointerConstraints() +{ + breakPointerConstraints(m_window ? m_window->surface() : nullptr); +} + +void PointerInputRedirection::disconnectConfinedPointerRegionConnection() +{ + disconnect(m_confinedPointerRegionConnection); + m_confinedPointerRegionConnection = QMetaObject::Connection(); +} + +void PointerInputRedirection::disconnectPointerConstraintsConnection() +{ + disconnect(m_constraintsConnection); + m_constraintsConnection = QMetaObject::Connection(); +} + +template +static QRegion getConstraintRegion(Toplevel *t, T *constraint) +{ + const QRegion windowShape = t->inputShape(); + const QRegion windowRegion = windowShape.isEmpty() ? QRegion(0, 0, t->clientSize().width(), t->clientSize().height()) : windowShape; + const QRegion intersected = constraint->region().isEmpty() ? windowRegion : windowRegion.intersected(constraint->region()); + return intersected.translated(t->pos() + t->clientPos()); +} + +void PointerInputRedirection::enablePointerConstraints() +{ + if (m_window.isNull()) { + return; + } + const auto s = m_window->surface(); + if (!s) { + return; + } + if (s != waylandServer()->seat()->focusedPointerSurface()) { + return; + } + if (!supportsWarping()) { + return; + } + if (m_blockConstraint) { + return; + } + const auto cf = s->confinedPointer(); + if (cf) { + if (cf->isConfined()) { + return; + } + const QRegion r = getConstraintRegion(m_window.data(), cf.data()); + if (r.contains(m_pos.toPoint())) { + cf->setConfined(true); + m_confined = true; + m_confinedPointerRegionConnection = connect(cf, &KWayland::Server::ConfinedPointerInterface::regionChanged, this, + [this] { + if (!m_window) { + return; + } + const auto s = m_window->surface(); + if (!s) { + return; + } + const auto cf = s->confinedPointer(); + if (!getConstraintRegion(m_window.data(), cf.data()).contains(m_pos.toPoint())) { + // pointer no longer in confined region, break the confinement + cf->setConfined(false); + m_confined = false; + } else { + if (!cf->isConfined()) { + cf->setConfined(true); + m_confined = true; + } + } + } + ); + // TODO: show notification + return; + } + } else { + disconnectConfinedPointerRegionConnection(); + } + const auto lock = s->lockedPointer(); + if (lock) { + if (lock->isLocked()) { + return; + } + const QRegion r = getConstraintRegion(m_window.data(), lock.data()); + if (r.contains(m_pos.toPoint())) { + lock->setLocked(true); + m_locked = true; + // TODO: show notification + // TODO: connect to region change - is it needed at all? If the pointer is locked it's always in the region + } + } +} + void PointerInputRedirection::warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInterface *newSurface) { auto xc = waylandServer()->xWaylandConnection(); @@ -519,8 +645,47 @@ void PointerInputRedirection::warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInte xcb_flush(connection()); } +QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) const +{ + if (!m_window) { + return pos; + } + auto s = m_window->surface(); + if (!s) { + return pos; + } + auto cf = s->confinedPointer(); + if (!cf) { + return pos; + } + if (!cf->isConfined()) { + return pos; + } + + const QRegion confinementRegion = getConstraintRegion(m_window.data(), cf.data()); + if (confinementRegion.contains(pos.toPoint())) { + return pos; + } + QPointF p = pos; + // allow either x or y to pass + p = QPointF(m_pos.x(), pos.y()); + if (confinementRegion.contains(p.toPoint())) { + return p; + } + p = QPointF(pos.x(), m_pos.y()); + if (confinementRegion.contains(p.toPoint())) { + return p; + } + + return m_pos; +} + void PointerInputRedirection::updatePosition(const QPointF &pos) { + if (m_locked) { + // locked pointer should not move + return; + } // verify that at least one screen contains the pointer position QPointF p = pos; if (!screenContainsPos(p)) { @@ -533,6 +698,15 @@ void PointerInputRedirection::updatePosition(const QPointF &pos) } } } + p = applyPointerConfinement(p); + if (p == m_pos) { + // didn't change due to confinement + return; + } + // verify screen confinement + if (!screenContainsPos(p)) { + return; + } m_pos = p; emit m_input->globalPointerChanged(m_pos); } diff --git a/pointer_input.h b/pointer_input.h index 1697304780..d1b334c465 100644 --- a/pointer_input.h +++ b/pointer_input.h @@ -84,6 +84,16 @@ public: void setWindowSelectionCursor(const QByteArray &shape); void removeWindowSelectionCursor(); + void enablePointerConstraints(); + void breakPointerConstraints(); + void blockPointerConstraints() { + m_blockConstraint = true; + } + + bool isConstrained() const { + return m_confined || m_locked; + } + /** * @internal */ @@ -139,6 +149,10 @@ private: void updatePosition(const QPointF &pos); void updateButton(uint32_t button, InputRedirection::PointerButtonState state); void warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInterface *surface); + QPointF applyPointerConfinement(const QPointF &pos) const; + void disconnectConfinedPointerRegionConnection(); + void disconnectPointerConstraintsConnection(); + void breakPointerConstraints(KWayland::Server::SurfaceInterface *surface); CursorImage *m_cursor; bool m_inited = false; bool m_supportsWarping; @@ -147,6 +161,11 @@ private: Qt::MouseButtons m_qtButtons; QMetaObject::Connection m_windowGeometryConnection; QMetaObject::Connection m_internalWindowConnection; + QMetaObject::Connection m_constraintsConnection; + QMetaObject::Connection m_confinedPointerRegionConnection; + bool m_confined = false; + bool m_locked = false; + bool m_blockConstraint = false; }; class CursorImage : public QObject diff --git a/wayland_server.cpp b/wayland_server.cpp index 53771ee438..b9e909fa8b 100644 --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -40,6 +40,7 @@ along with this program. If not, see . #include #include #include +#include #include #include #include @@ -186,6 +187,7 @@ bool WaylandServer::init(const QByteArray &socketName, InitalizationFlags flags) m_seat = m_display->createSeat(m_display); m_seat->create(); m_display->createPointerGestures(PointerGesturesInterfaceVersion::UnstableV1, m_display)->create(); + m_display->createPointerConstraints(PointerConstraintsInterfaceVersion::UnstableV1, m_display)->create(); auto ddm = m_display->createDataDeviceManager(m_display); ddm->create(); connect(ddm, &DataDeviceManagerInterface::dataDeviceCreated, this,