pointer_input: implement edge barrier between screens

Allow users to configure a virtual edge barrier between screens.
The pointer will only cross over to the other screen after the distance
travelled surpasses edgeBarrier.

Reduce the speed during interactive moveresize, at edges that trigger,
and at the corner.

Only supports wayland. Doesn't have X11 support since it is far too
complicated there.

BUG: 416570
BUG: 451744
This commit is contained in:
Yifan Zhu 2024-02-10 15:23:57 -08:00
parent ea4fa87bc6
commit ad13765348
12 changed files with 336 additions and 3 deletions

View file

@ -82,6 +82,10 @@ WaylandTestApplication::WaylandTestApplication(OperationMode mode, int &argc, ch
KConfigGroup windowsGroup = config->group(QStringLiteral("Windows"));
windowsGroup.writeEntry("Placement", Placement::policyToString(PlacementSmart));
windowsGroup.sync();
KConfigGroup edgeBarrierGroup = config->group(QStringLiteral("EdgeBarrier"));
edgeBarrierGroup.writeEntry("EdgeBarrier", 0);
edgeBarrierGroup.writeEntry("CornerBarrier", false);
edgeBarrierGroup.sync();
setConfig(config);
const auto ownPath = libraryPaths().last();

View file

@ -94,6 +94,8 @@ private Q_SLOTS:
void testWindowUnderCursorWhileButtonPressed();
void testConfineToScreenGeometry_data();
void testConfineToScreenGeometry();
void testEdgeBarrier_data();
void testEdgeBarrier();
void testResizeCursor_data();
void testResizeCursor();
void testMoveCursor();
@ -145,6 +147,11 @@ void PointerInputTest::init()
m_compositor = Test::waylandCompositor();
m_seat = Test::waylandSeat();
auto group = kwinApp()->config()->group(QStringLiteral("EdgeBarrier"));
group.writeEntry("EdgeBarrier", 0);
group.writeEntry("CornerBarrier", false);
group.sync();
Workspace::self()->slotReconfigure();
workspace()->setActiveOutput(QPoint(640, 512));
input()->pointer()->warp(QPoint(640, 512));
}
@ -1542,6 +1549,88 @@ void PointerInputTest::testConfineToScreenGeometry()
QCOMPARE(Cursors::self()->mouse()->pos(), expectedPos);
}
void PointerInputTest::testEdgeBarrier_data()
{
QTest::addColumn<QPoint>("startPos");
QTest::addColumn<QList<QPoint>>("movements");
QTest::addColumn<int>("targetOutputId");
QTest::addColumn<bool>("cornerBarrier");
// screen layout:
//
// +----------+----------+---------+
// | left | top | right |
// +----------+----------+---------+
// | bottom |
// +----------+
//
QTest::newRow("move right - barred") << QPoint(1270, 512) << QList<QPoint>{QPoint(20, 0)} << 0 << false;
QTest::newRow("move left - barred") << QPoint(1290, 512) << QList<QPoint>{QPoint(-20, 0)} << 1 << false;
QTest::newRow("move down - barred") << QPoint(1920, 1014) << QList<QPoint>{QPoint(0, 20)} << 1 << false;
QTest::newRow("move up - barred") << QPoint(1920, 1034) << QList<QPoint>{QPoint(0, -20)} << 3 << false;
QTest::newRow("move top-right - barred") << QPoint(2550, 1034) << QList<QPoint>{QPoint(20, -20)} << 3 << false;
QTest::newRow("move top-left - barred") << QPoint(1290, 1034) << QList<QPoint>{QPoint(-20, -20)} << 3 << false;
QTest::newRow("move bottom-right - barred") << QPoint(1270, 1014) << QList<QPoint>{QPoint(20, 20)} << 0 << false;
QTest::newRow("move bottom-left - barred") << QPoint(2570, 1014) << QList<QPoint>{QPoint(-20, 20)} << 2 << false;
QTest::newRow("move right - not barred") << QPoint(1270, 512) << QList<QPoint>{QPoint(100, 0)} << 1 << false;
QTest::newRow("move left - not barred") << QPoint(1290, 512) << QList<QPoint>{QPoint(-100, 0)} << 0 << false;
QTest::newRow("move down - not barred") << QPoint(1920, 1014) << QList<QPoint>{QPoint(0, 100)} << 3 << false;
QTest::newRow("move up - not barred") << QPoint(1920, 1034) << QList<QPoint>{QPoint(0, -100)} << 1 << false;
QTest::newRow("move top-right - not barred") << QPoint(2550, 1034) << QList<QPoint>{QPoint(100, -100)} << 2 << false;
QTest::newRow("move top-left - not barred") << QPoint(1290, 1034) << QList<QPoint>{QPoint(-100, -100)} << 0 << false;
QTest::newRow("move bottom-right - not barred") << QPoint(1270, 1014) << QList<QPoint>{QPoint(100, 100)} << 3 << false;
QTest::newRow("move bottom-left - not barred") << QPoint(2570, 1014) << QList<QPoint>{QPoint(-100, 100)} << 3 << false;
QTest::newRow("move cumulative") << QPoint(1279, 512) << QList<QPoint>{QPoint(24, 0), QPoint(24, 0)} << 1 << false;
QTest::newRow("move then idle") << QPoint(1279, 512) << QList<QPoint>{QPoint(24, 0), QPoint(0, 0), QPoint(0, 0), QPoint(3, 0)} << 0 << false;
QTest::newRow("move top-right - corner barrier") << QPoint(2550, 1034) << QList<QPoint>{QPoint(100, -100)} << 3 << true;
QTest::newRow("move top-left - corner barrier") << QPoint(1290, 1034) << QList<QPoint>{QPoint(-100, -100)} << 3 << true;
QTest::newRow("move bottom-right - corner barrier") << QPoint(1270, 1014) << QList<QPoint>{QPoint(100, 100)} << 0 << true;
QTest::newRow("move bottom-left - corner barrier") << QPoint(2570, 1014) << QList<QPoint>{QPoint(-100, 100)} << 2 << true;
}
void PointerInputTest::testEdgeBarrier()
{
// setup screen layout
const QList<QRect> geometries{
QRect(0, 0, 1280, 1024),
QRect(1280, 0, 1280, 1024),
QRect(2560, 0, 1280, 1024),
QRect(1280, 1024, 1280, 1024)};
Test::setOutputConfig(geometries);
const auto outputs = workspace()->outputs();
QCOMPARE(outputs.count(), geometries.count());
QCOMPARE(outputs[0]->geometry(), geometries.at(0));
QCOMPARE(outputs[1]->geometry(), geometries.at(1));
QCOMPARE(outputs[2]->geometry(), geometries.at(2));
QCOMPARE(outputs[3]->geometry(), geometries.at(3));
QFETCH(QPoint, startPos);
input()->pointer()->warp(startPos);
quint32 timestamp = waylandServer()->seat()->timestamp().count() + 5000;
Test::pointerMotionRelative(QPoint(0, 0), timestamp);
timestamp += 1000;
QCOMPARE(Cursors::self()->mouse()->pos(), startPos);
auto group = kwinApp()->config()->group(QStringLiteral("EdgeBarrier"));
group.writeEntry("EdgeBarrier", 25);
QFETCH(bool, cornerBarrier);
group.writeEntry("CornerBarrier", cornerBarrier);
group.sync();
workspace()->slotReconfigure();
QFETCH(QList<QPoint>, movements);
for (const auto &movement : movements) {
Test::pointerMotionRelative(movement, timestamp);
timestamp += 1000;
}
QFETCH(int, targetOutputId);
QCOMPARE(workspace()->outputAt(Cursors::self()->mouse()->pos()), workspace()->outputs().at(targetOutputId));
}
void PointerInputTest::testResizeCursor_data()
{
QTest::addColumn<Qt::Edges>("edges");

View file

@ -133,6 +133,10 @@ void ScreensTest::testCurrentWithFollowsMouse()
auto group = kwinApp()->config()->group(QStringLiteral("Windows"));
group.writeEntry("ActiveMouseScreen", true);
group.sync();
auto edgeBarrierGroup = kwinApp()->config()->group(QStringLiteral("EdgeBarrier"));
edgeBarrierGroup.writeEntry("EdgeBarrier", 0);
edgeBarrierGroup.writeEntry("CornerBarrier", false);
edgeBarrierGroup.sync();
workspace()->slotReconfigure();
QFETCH(QList<QRect>, geometries);

View file

@ -82,4 +82,14 @@
<default>false</default>
</entry>
</group>
<group name="EdgeBarrier">
<entry name="CornerBarrier" type="Bool">
<default>true</default>
</entry>
<entry name="EdgeBarrier" type="Int">
<default>100</default>
<min>0</min>
<max>1000</max>
</entry>
</group>
</kcfg>

View file

@ -264,6 +264,61 @@
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="CornerBarrierLabel">
<property name="text">
<string>&amp;Corner barrier:</string>
</property>
<property name="buddy">
<cstring>kcfg_CornerBarrier</cstring>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="kcfg_CornerBarrier">
<property name="whatsThis">
<string>Here you can enable or disable the virtual corner barrier between screens. The barrier prevents the cursor from moving to another screen when it is already touching a screen corner. This makes it easier to trigger user interface elements like maximized windows' close buttons when using multiple screens.</string>
</property>
<property name="toolTip">
<string comment="@info:tooltip">Prevents cursors from crossing at screen corners.</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="EdgeBarrierLabel">
<property name="text">
<string>&amp;Edge barrier:</string>
</property>
<property name="buddy">
<cstring>kcfg_EdgeBarrier</cstring>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QSpinBox" name="kcfg_EdgeBarrier">
<property name="whatsThis">
<string>Here you can set size of the edge barrier between different screens. The barrier adds additional distance you have to move your pointer before it crosses the edge onto the other screen. This makes it easier to access user interface elements like Plasma Panels that are located on an edge between screens.</string>
</property>
<property name="toolTip">
<string comment="@info:tooltip">Additional distance cursor needs to travel to cross screen edges.</string>
</property>
<property name="specialValueText">
<string>None</string>
</property>
<property name="suffix">
<string> px</string>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>1000</number>
</property>
<property name="value">
<number>100</number>
</property>
</widget>
</item>
</layout>
</item>
<item>

View file

@ -224,6 +224,16 @@
<default>false</default>
</entry>
</group>
<group name="EdgeBarrier">
<entry name="CornerBarrier" type="Bool">
<default>true</default>
</entry>
<entry name="EdgeBarrier" type="Int">
<default>100</default>
<min>0</min>
<max>1000</max>
</entry>
</group>
<group name="ScreenEdges">
<entry name="RemainActiveOnFullscreen" type="Bool">
<default>false</default>

View file

@ -48,6 +48,8 @@ Options::Options(QObject *parent)
, m_windowSnapZone(0)
, m_centerSnapZone(0)
, m_snapOnlyWhenOverlapping(false)
, m_edgeBarrier(0)
, m_cornerBarrier(0)
, m_rollOverDesktops(false)
, m_focusStealingPreventionLevel(0)
, m_killPingTimeout(0)
@ -309,6 +311,24 @@ void Options::setSnapOnlyWhenOverlapping(bool snapOnlyWhenOverlapping)
Q_EMIT snapOnlyWhenOverlappingChanged();
}
void Options::setEdgeBarrier(int edgeBarrier)
{
if (m_edgeBarrier == edgeBarrier) {
return;
}
m_edgeBarrier = edgeBarrier;
Q_EMIT edgeBarrierChanged();
}
void Options::setCornerBarrier(bool cornerBarrier)
{
if (m_cornerBarrier == cornerBarrier) {
return;
}
m_cornerBarrier = cornerBarrier;
Q_EMIT cornerBarrierChanged();
}
void Options::setRollOverDesktops(bool rollOverDesktops)
{
if (m_rollOverDesktops == rollOverDesktops) {
@ -848,6 +868,8 @@ void Options::syncFromKcfgc()
setBorderSnapZone(m_settings->borderSnapZone());
setWindowSnapZone(m_settings->windowSnapZone());
setCenterSnapZone(m_settings->centerSnapZone());
setEdgeBarrier(m_settings->edgeBarrier());
setCornerBarrier(m_settings->cornerBarrier());
setSnapOnlyWhenOverlapping(m_settings->snapOnlyWhenOverlapping());
setKillPingTimeout(m_settings->killPingTimeout());
setHideUtilityWindowsForInactive(m_settings->hideUtilityWindowsForInactive());

View file

@ -127,6 +127,14 @@ class KWIN_EXPORT Options : public QObject
* Snap only when windows will overlap.
*/
Q_PROPERTY(bool snapOnlyWhenOverlapping READ isSnapOnlyWhenOverlapping WRITE setSnapOnlyWhenOverlapping NOTIFY snapOnlyWhenOverlappingChanged)
/**
* The size of the virtual barrier at edges between screens.
*/
Q_PROPERTY(int edgeBarrier READ edgeBarrier WRITE setEdgeBarrier NOTIFY edgeBarrierChanged)
/**
* Whether to enable a cursor barrier at the corners of the screen.
*/
Q_PROPERTY(int cornerBarrier READ cornerBarrier WRITE setCornerBarrier NOTIFY cornerBarrierChanged)
/**
* Whether or not we roll over to the other edge when switching desktops past the edge.
*/
@ -383,6 +391,22 @@ public:
return m_snapOnlyWhenOverlapping;
}
/**
* The size of the virtual barrier at edges between screens.
*/
int edgeBarrier() const
{
return m_edgeBarrier;
}
/**
* Whether to enable a cursor barrier at the corners of the screen.
*/
int cornerBarrier() const
{
return m_cornerBarrier;
}
/**
* Whether or not we roll over to the other edge when switching desktops past the edge.
*/
@ -712,6 +736,8 @@ public:
void setWindowSnapZone(int windowSnapZone);
void setCenterSnapZone(int centerSnapZone);
void setSnapOnlyWhenOverlapping(bool snapOnlyWhenOverlapping);
void setEdgeBarrier(int edgeBarrier);
void setCornerBarrier(bool cornerBarrier);
void setRollOverDesktops(bool rollOverDesktops);
void setFocusStealingPreventionLevel(int focusStealingPreventionLevel);
void setOperationTitlebarDblClick(WindowOperation operationTitlebarDblClick);
@ -913,6 +939,8 @@ Q_SIGNALS:
void windowSnapZoneChanged();
void centerSnapZoneChanged();
void snapOnlyWhenOverlappingChanged();
void edgeBarrierChanged();
void cornerBarrierChanged();
void rollOverDesktopsChanged(bool enabled);
void focusStealingPreventionLevelChanged();
void operationTitlebarDblClickChanged();
@ -975,6 +1003,8 @@ private:
int m_windowSnapZone;
int m_centerSnapZone;
bool m_snapOnlyWhenOverlapping;
int m_edgeBarrier;
bool m_cornerBarrier;
bool m_rollOverDesktops;
int m_focusStealingPreventionLevel;
int m_killPingTimeout;

View file

@ -21,6 +21,7 @@
#include "input_event_spy.h"
#include "mousebuttons.h"
#include "osd.h"
#include "screenedge.h"
#include "wayland/display.h"
#include "wayland/pointer.h"
#include "wayland/pointerconstraints_v1.h"
@ -240,7 +241,7 @@ void PointerInputRedirection::processMotionInternal(const QPointF &pos, const QP
}
PositionUpdateBlocker blocker(this);
updatePosition(pos);
updatePosition(pos, time);
MouseEvent event(QEvent::MouseMove, m_pos, Qt::NoButton, m_qtButtons,
input()->keyboardModifiers(), time,
delta, deltaNonAccelerated, device);
@ -736,8 +737,87 @@ QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) con
return m_pos;
}
void PointerInputRedirection::updatePosition(const QPointF &pos)
PointerInputRedirection::EdgeBarrierType PointerInputRedirection::edgeBarrierType(const QPointF &pos, const QRectF &lastOutputGeometry) const
{
constexpr qreal cornerThreshold = 15;
const auto moveResizeWindow = workspace()->moveResizeWindow();
const bool onCorner = (pos - lastOutputGeometry.topLeft()).manhattanLength() <= cornerThreshold
|| (pos - lastOutputGeometry.bottomLeft()).manhattanLength() <= cornerThreshold
|| (pos - lastOutputGeometry.topRight()).manhattanLength() <= cornerThreshold
|| (pos - lastOutputGeometry.bottomRight()).manhattanLength() <= cornerThreshold;
if (moveResizeWindow && moveResizeWindow->isInteractiveMove()) {
return EdgeBarrierType::WindowMoveBarrier;
} else if (moveResizeWindow && moveResizeWindow->isInteractiveResize()) {
return EdgeBarrierType::WindowResizeBarrier;
} else if (options->cornerBarrier() && onCorner) {
return EdgeBarrierType::CornerBarrier;
} else if (workspace()->screenEdges()->inApproachGeometry(pos.toPoint())) {
return EdgeBarrierType::EdgeElementBarrier;
} else {
return EdgeBarrierType::NormalBarrier;
}
}
qreal PointerInputRedirection::edgeBarrier(EdgeBarrierType type) const
{
const auto barrierWidth = options->edgeBarrier();
switch (type) {
case EdgeBarrierType::WindowMoveBarrier:
case EdgeBarrierType::WindowResizeBarrier:
return 1.5 * barrierWidth;
case EdgeBarrierType::EdgeElementBarrier:
return 2 * barrierWidth;
case EdgeBarrierType::CornerBarrier:
return 2000;
case EdgeBarrierType::NormalBarrier:
return barrierWidth;
default:
Q_UNREACHABLE();
return 0;
}
}
QPointF PointerInputRedirection::applyEdgeBarrier(const QPointF &pos, const Output *currentOutput, std::chrono::microseconds time)
{
auto softThreshold = [](qreal value, qreal threshold) {
return std::abs(value) < threshold ? 0 : value - (value > 0 ? threshold : -threshold);
};
// optimization to avoid looping over all outputs
if (exclusiveContains(currentOutput->geometry(), m_pos)) {
m_movementInEdgeBarrier = QPointF();
return pos;
}
const Output *lastOutput = workspace()->outputAt(m_pos);
QPointF newPos = confineToBoundingBox(pos, lastOutput->geometry());
const auto type = edgeBarrierType(newPos, lastOutput->geometry());
if (m_lastEdgeBarrierType != type) {
m_movementInEdgeBarrier = QPointF();
}
m_lastEdgeBarrierType = type;
const auto barrierWidth = edgeBarrier(type);
const qreal returnSpeed = barrierWidth / 10.0 /* px/s */ / 1000'000.0; // px/us
std::chrono::microseconds timeDiff(time - m_lastMoveTime);
qreal returnDistance = returnSpeed * timeDiff.count();
m_movementInEdgeBarrier += (pos - newPos);
m_movementInEdgeBarrier.setX(softThreshold(m_movementInEdgeBarrier.x(), returnDistance));
m_movementInEdgeBarrier.setY(softThreshold(m_movementInEdgeBarrier.y(), returnDistance));
if (std::abs(m_movementInEdgeBarrier.x()) > barrierWidth) {
newPos.rx() += softThreshold(m_movementInEdgeBarrier.x(), barrierWidth);
m_movementInEdgeBarrier.setX(0);
}
if (std::abs(m_movementInEdgeBarrier.y()) > barrierWidth) {
newPos.ry() += softThreshold(m_movementInEdgeBarrier.y(), barrierWidth);
m_movementInEdgeBarrier.setY(0);
}
return newPos;
}
void PointerInputRedirection::updatePosition(const QPointF &pos, std::chrono::microseconds time)
{
m_lastMoveTime = time;
if (m_locked) {
// locked pointer should not move
return;
@ -745,6 +825,7 @@ void PointerInputRedirection::updatePosition(const QPointF &pos)
// verify that at least one screen contains the pointer position
const Output *currentOutput = workspace()->outputAt(pos);
QPointF p = confineToBoundingBox(pos, currentOutput->geometry());
p = applyEdgeBarrier(p, currentOutput, time);
p = applyPointerConfinement(p);
if (p == m_pos) {
// didn't change due to confinement

View file

@ -144,6 +144,14 @@ public:
void processFrame(KWin::InputDevice *device = nullptr);
private:
enum class EdgeBarrierType {
NormalBarrier,
WindowMoveBarrier,
// WindowResize is separate from WindowMove since there is edge snapping during resize, so a different resistance might be desirable
WindowResizeBarrier,
EdgeElementBarrier,
CornerBarrier,
};
void processMotionInternal(const QPointF &pos, const QPointF &delta, const QPointF &deltaNonAccelerated, std::chrono::microseconds time, InputDevice *device);
void cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) override;
@ -153,8 +161,11 @@ private:
void updateOnStartMoveResize();
void updateToReset();
void updatePosition(const QPointF &pos);
void updatePosition(const QPointF &pos, std::chrono::microseconds time);
void updateButton(uint32_t button, InputRedirection::PointerButtonState state);
QPointF applyEdgeBarrier(const QPointF &pos, const Output *currentOutput, std::chrono::microseconds time);
EdgeBarrierType edgeBarrierType(const QPointF &pos, const QRectF &lastOutputGeometry) const;
qreal edgeBarrier(EdgeBarrierType type) const;
QPointF applyPointerConfinement(const QPointF &pos) const;
void disconnectConfinedPointerRegionConnection();
void disconnectLockedPointerAboutToBeUnboundConnection();
@ -176,7 +187,10 @@ private:
bool m_locked = false;
bool m_enableConstraints = true;
bool m_lastOutputWasPlaceholder = true;
QPointF m_movementInEdgeBarrier;
std::chrono::microseconds m_lastMoveTime = std::chrono::microseconds::zero();
friend class PositionUpdateBlocker;
EdgeBarrierType m_lastEdgeBarrierType = EdgeBarrierType::NormalBarrier;
};
class WaylandCursorImage : public QObject

View file

@ -1424,6 +1424,16 @@ void ScreenEdges::check(const QPoint &pos, const QDateTime &now, bool forceNoPus
}
}
bool ScreenEdges::inApproachGeometry(const QPoint &pos) const
{
for (const auto &edge : m_edges) {
if (edge->approachGeometry().contains(pos)) {
return true;
}
}
return false;
}
bool ScreenEdges::isEntered(QMouseEvent *event)
{
if (event->type() != QEvent::MouseMove) {

View file

@ -253,6 +253,10 @@ public:
* @param forceNoPushBack needs to be called to workaround some DnD clients, don't use unless you want to chek on a DnD event
*/
void check(const QPoint &pos, const QDateTime &now, bool forceNoPushBack = false);
/**
* Check, if @p pos is in the approach geometry of any edge.
*/
bool inApproachGeometry(const QPoint &pos) const;
/**
* The (dpi dependent) length, reserved for the active corners of each edge - 1/3"
*/