Implement support for pointer constraints
Summary: There are two types of constraints supported: 1. Pointer confinement 2. Pointer locking In the case of confinement the pointer is confined to a given region of the surface. This is comparable to general operation where the pointer is confined to the screen region. In the second case the pointer gets locked. That means it cannot move at all. No further position updates are provided, only relative motion events can go to the application. There is a hint about cursor position update on unlock which is not yet implemented in KWayland::Server, thus also not in this change. The implementation in KWin grants the requests for pointer constraints when the pointer enters the constrained region, either by pointer movement or by e.g. stacking order changes. There is no confirmation from user required to enter that mode. But we want to show an OSD when the pointer gets constrained, this is not yet implemented, though. Breaking an active constraint is relatively easy. E.g. changing the stacking order will break the constraint if another surface is under the cursor. Also (in case of confinement) moving the pointer to an overlapping window breaks the confinement. But as soon as one moves the pointer back to the window a constraint might get honoured again. To properly break there is a dedicated event filter. It listens for a long press of the Escape key. If hold for 3sec the pointer constraint is broken and not activated again till the pointer got moved out of the window. Afterward when moving in the pointer might activate again. The escape filter ensures that the key press is forwarded to the application if it's a short press or if another key gets pressed during the three seconds. If the three seconds way fires, the later escape release is not sent to the application. This basic interaction is also ensured through an added auto test. This change implements T4605. Test Plan: Added auto test and nested KWin Wayland with D3488 Reviewers: #kwin, #plasma_on_wayland Subscribers: plasma-devel, kwin Tags: #plasma_on_wayland, #kwin Differential Revision: https://phabricator.kde.org/D3506
This commit is contained in:
parent
2defd6bb64
commit
0c5ca405cc
10 changed files with 673 additions and 2 deletions
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
379
autotests/integration/pointer_constraints_test.cpp
Normal file
379
autotests/integration/pointer_constraints_test.cpp
Normal file
|
@ -0,0 +1,379 @@
|
|||
/********************************************************************
|
||||
KWin - the KDE window manager
|
||||
This file is part of the KDE project.
|
||||
|
||||
Copyright (C) 2016 Martin Gräßlin <mgraesslin@kde.org>
|
||||
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*********************************************************************/
|
||||
#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 <KWayland/Client/compositor.h>
|
||||
#include <KWayland/Client/keyboard.h>
|
||||
#include <KWayland/Client/pointer.h>
|
||||
#include <KWayland/Client/pointerconstraints.h>
|
||||
#include <KWayland/Client/region.h>
|
||||
#include <KWayland/Client/shell.h>
|
||||
#include <KWayland/Client/seat.h>
|
||||
#include <KWayland/Client/shm_pool.h>
|
||||
#include <KWayland/Client/surface.h>
|
||||
#include <KWayland/Server/seat_interface.h>
|
||||
|
||||
#include <linux/input.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
using namespace KWin;
|
||||
using namespace KWayland::Client;
|
||||
|
||||
typedef std::function<QPoint(const QRect&)> 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<PointerFunc>();
|
||||
qRegisterMetaType<KWin::ShellClient*>();
|
||||
qRegisterMetaType<KWin::AbstractClient*>();
|
||||
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<Test::ShellSurfaceType>("type");
|
||||
QTest::addColumn<PointerFunc>("positionFunction");
|
||||
QTest::addColumn<int>("xOffset");
|
||||
QTest::addColumn<int>("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> surface(Test::createSurface());
|
||||
QFETCH(Test::ShellSurfaceType, type);
|
||||
QScopedPointer<QObject> shellSurface(Test::createShellSurface(type, surface.data()));
|
||||
QScopedPointer<Pointer> pointer(Test::waylandSeat()->createPointer());
|
||||
QScopedPointer<ConfinedPointer> 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<Surface> surface2(Test::createSurface());
|
||||
QScopedPointer<QObject> 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<Test::ShellSurfaceType>("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> surface(Test::createSurface());
|
||||
QFETCH(Test::ShellSurfaceType, type);
|
||||
QScopedPointer<QObject> shellSurface(Test::createShellSurface(type, surface.data()));
|
||||
QScopedPointer<Pointer> pointer(Test::waylandSeat()->createPointer());
|
||||
QScopedPointer<LockedPointer> 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<Test::ShellSurfaceType>("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> surface(Test::createSurface());
|
||||
QFETCH(Test::ShellSurfaceType, type);
|
||||
QScopedPointer<QObject> shellSurface(Test::createShellSurface(type, surface.data()));
|
||||
QScopedPointer<Pointer> pointer(Test::waylandSeat()->createPointer());
|
||||
QScopedPointer<Keyboard> 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> 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>(), 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>(), quint32(KEY_LEFTSHIFT));
|
||||
QVERIFY(!unlockedSpy.wait());
|
||||
kwinApp()->platform()->keyboardKeyReleased(KEY_ESC, timestamp++);
|
||||
QVERIFY(keyChangedSpy.wait());
|
||||
QCOMPARE(keyChangedSpy.last().at(0).value<quint32>(), 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"
|
|
@ -28,6 +28,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
#include <KWayland/Client/registry.h>
|
||||
#include <KWayland/Client/plasmashell.h>
|
||||
#include <KWayland/Client/plasmawindowmanagement.h>
|
||||
#include <KWayland/Client/pointerconstraints.h>
|
||||
#include <KWayland/Client/seat.h>
|
||||
#include <KWayland/Client/server_decoration.h>
|
||||
#include <KWayland/Client/shell.h>
|
||||
|
@ -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) {
|
||||
|
|
73
input.cpp
73
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<QTimer> 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<MouseEvent*>(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)
|
||||
|
|
4
input.h
4
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<void(const QPoint &)> 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<InputEventFilter*> m_filters;
|
||||
KSharedConfigPtr m_inputConfig;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -35,6 +35,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
#include <KWayland/Server/buffer_interface.h>
|
||||
#include <KWayland/Server/datadevice_interface.h>
|
||||
#include <KWayland/Server/display.h>
|
||||
#include <KWayland/Server/pointerconstraints_interface.h>
|
||||
#include <KWayland/Server/seat_interface.h>
|
||||
#include <KWayland/Server/surface_interface.h>
|
||||
// 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<AbstractClient*>(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 <typename T>
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -40,6 +40,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
#include <KWayland/Server/output_interface.h>
|
||||
#include <KWayland/Server/plasmashell_interface.h>
|
||||
#include <KWayland/Server/plasmawindowmanagement_interface.h>
|
||||
#include <KWayland/Server/pointerconstraints_interface.h>
|
||||
#include <KWayland/Server/pointergestures_interface.h>
|
||||
#include <KWayland/Server/qtsurfaceextension_interface.h>
|
||||
#include <KWayland/Server/seat_interface.h>
|
||||
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue