Skip to content

Commit 1055fab

Browse files
authored
Merge pull request #408 from taj-ny/swipe-angle
core: make swipe triggers angle-based
2 parents 9e37b00 + d149385 commit 1055fab

26 files changed

+939
-53
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ if (INPUTACTIONS_BUILD_HYPRLAND OR INPUTACTIONS_BUILD_KWIN OR INPUTACTIONS_BUILD
8888
libinputactions/triggers/MotionTrigger.cpp
8989
libinputactions/triggers/PressTrigger.cpp
9090
libinputactions/triggers/StrokeTrigger.cpp
91+
libinputactions/triggers/SwipeTrigger.cpp
9192
libinputactions/triggers/Trigger.cpp
9293
libinputactions/triggers/WheelTrigger.cpp
9394
libinputactions/variables/LocalVariable.cpp

src/libinputactions/config/parsers/core.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#include <libinputactions/triggers/KeyboardShortcutTrigger.h>
5353
#include <libinputactions/triggers/PressTrigger.h>
5454
#include <libinputactions/triggers/StrokeTrigger.h>
55+
#include <libinputactions/triggers/SwipeTrigger.h>
5556
#include <libinputactions/triggers/WheelTrigger.h>
5657
#include <libinputactions/variables/Variable.h>
5758
#include <libinputactions/variables/VariableManager.h>
@@ -436,11 +437,14 @@ void NodeParser<InputDeviceProperties>::parse(const Node *node, InputDevicePrope
436437
loadSetter(result, &InputDeviceProperties::setGrab, node->at("grab"));
437438
loadSetter(result, &InputDeviceProperties::setHandleLibevdevEvents, node->at("handle_libevdev_events"));
438439
loadSetter(result, &InputDeviceProperties::setIgnore, node->at("ignore"));
440+
loadSetter(result, &InputDeviceProperties::setMotionThreshold, node->at("motion_threshold"));
439441
loadSetter(result, &InputDeviceProperties::setMouseMotionTimeout, node->at("motion_timeout"));
440442
loadSetter(result, &InputDeviceProperties::setMousePressTimeout, node->at("press_timeout"));
441443
loadSetter(result, &InputDeviceProperties::setMouseUnblockButtonsOnTimeout, node->at("unblock_buttons_on_timeout"));
442444
loadSetter(result, &InputDeviceProperties::setTouchpadButtonPad, node->at("buttonpad"));
443445
loadSetter(result, &InputDeviceProperties::setTouchpadClickTimeout, node->at("click_timeout"));
446+
loadSetter(result, &InputDeviceProperties::setTouchpadMotionThreshold2, node->at("motion_threshold_2"));
447+
loadSetter(result, &InputDeviceProperties::setTouchpadMotionThreshold2, node->at("motion_threshold_3"));
444448

445449
if (const auto *pressureRangesNode = node->mapAt("pressure_ranges")) {
446450
loadSetter(result, &InputDeviceProperties::setFingerPressure, pressureRangesNode->at("finger"));
@@ -587,8 +591,20 @@ void NodeParser<std::unique_ptr<Trigger>>::parse(const Node *node, std::unique_p
587591
} else if (type == "stroke") {
588592
result = std::make_unique<StrokeTrigger>(node->at("strokes", true)->as<std::vector<Stroke>>(true));
589593
} else if (type == "swipe") {
590-
result = std::make_unique<DirectionalMotionTrigger>(TriggerType::Swipe,
591-
static_cast<TriggerDirection>(node->at("direction", true)->as<SwipeDirection>()));
594+
if (const auto *directionNode = node->at("direction")) {
595+
result = std::make_unique<SwipeTrigger>(directionNode->as<SwipeTriggerDirection>());
596+
} else {
597+
const auto *angleNode = node->at("angle", true);
598+
const auto angles = parseSeparatedString2<qreal>(angleNode, '-');
599+
if (angles.first > 360 || angles.second > 360) {
600+
throw InvalidValueConfigException(angleNode, "The angle may not be greater than 360.");
601+
}
602+
603+
auto swipeTrigger = std::make_unique<SwipeTrigger>(angles.first, angles.second);
604+
loadSetter(swipeTrigger.get(), &SwipeTrigger::setBidirectional, node->at("bidirectional"));
605+
result = std::move(swipeTrigger);
606+
}
607+
592608
} else if (type == "tap") {
593609
result = std::make_unique<Trigger>(TriggerType::Tap);
594610
} else if (type == "wheel") {

src/libinputactions/config/parsers/enums.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <libinputactions/globals.h>
2525
#include <libinputactions/interfaces/CursorShapeProvider.h>
2626
#include <libinputactions/triggers/DirectionalMotionTrigger.h>
27+
#include <libinputactions/triggers/SwipeTrigger.h>
2728
#include <unordered_map>
2829

2930
namespace InputActions
@@ -81,6 +82,22 @@ NODEPARSER_ENUM(SwipeDirection, "swipe direction",
8182
{"left_right", SwipeDirection::LeftRight},
8283
{"any", SwipeDirection::Any},
8384
}))
85+
NODEPARSER_ENUM(SwipeTriggerDirection, "swipe direction",
86+
(std::unordered_map<QString, SwipeTriggerDirection>{
87+
{"left", SwipeTriggerDirection::Left},
88+
{"right", SwipeTriggerDirection::Right},
89+
{"up", SwipeTriggerDirection::Up},
90+
{"down", SwipeTriggerDirection::Down},
91+
{"left_up", SwipeTriggerDirection::LeftUp},
92+
{"left_down", SwipeTriggerDirection::LeftDown},
93+
{"right_up", SwipeTriggerDirection::RightUp},
94+
{"right_down", SwipeTriggerDirection::RightDown},
95+
{"up_down", SwipeTriggerDirection::UpDown},
96+
{"left_right", SwipeTriggerDirection::LeftRight},
97+
{"left_up_right_down", SwipeTriggerDirection::LeftUpRightDown},
98+
{"left_down_right_up", SwipeTriggerDirection::LeftDownRightUp},
99+
{"any", SwipeTriggerDirection::Any},
100+
}))
84101
NODEPARSER_ENUM(TriggerSpeed, "trigger speed",
85102
(std::unordered_map<QString, TriggerSpeed>{
86103
{"fast", TriggerSpeed::Fast},

src/libinputactions/handlers/MotionTriggerHandler.cpp

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,13 @@
2828
#include <libinputactions/input/Delta.h>
2929
#include <libinputactions/input/devices/InputDevice.h>
3030
#include <libinputactions/triggers/StrokeTrigger.h>
31+
#include <libinputactions/triggers/SwipeTrigger.h>
3132

3233
Q_LOGGING_CATEGORY(INPUTACTIONS_HANDLER_MOTION, "inputactions.handler.motion", QtWarningMsg)
3334

3435
namespace InputActions
3536
{
3637

37-
/**
38-
* Minimum amount of deltas required to accurately detect axis changes.
39-
*/
40-
static const size_t AXIS_CHANGE_MIN_DELTA_COUNT = 10;
41-
4238
static const qreal CIRCLE_COASTING_FRICTION = 0.02;
4339
static const std::chrono::milliseconds CIRCLE_COASTING_TIMER_INTERVAL{30L};
4440
static const qreal PI_2 = M_PI * 2;
@@ -81,8 +77,15 @@ bool MotionTriggerHandler::handleMotion(const InputDevice *device, const PointDe
8177

8278
qCDebug(INPUTACTIONS_HANDLER_MOTION).nospace() << "Event (type: Motion, delta: " << delta.unaccelerated() << ")";
8379

84-
m_deltas.push_back(delta.unaccelerated());
85-
m_totalSwipeDelta += delta.unaccelerated();
80+
const auto hasStroke = hasActiveTriggers(TriggerType::Stroke);
81+
const auto hasSwipe = hasActiveTriggers(TriggerType::Swipe);
82+
83+
if (hasStroke) {
84+
m_deltas.push_back(delta.unaccelerated());
85+
}
86+
if (hasSwipe) {
87+
m_swipeDeltas.insert(m_swipeDeltas.begin(), delta.unaccelerated());
88+
}
8689

8790
TriggerSpeed speed{};
8891
if (!determineSpeed(TriggerType::Swipe, delta.unacceleratedHypot(), speed)) {
@@ -91,9 +94,8 @@ bool MotionTriggerHandler::handleMotion(const InputDevice *device, const PointDe
9194

9295
std::map<TriggerType, const TriggerUpdateEvent *> events;
9396
DirectionalMotionTriggerUpdateEvent circleEvent;
94-
DirectionalMotionTriggerUpdateEvent swipeEvent;
97+
SwipeTriggerUpdateEvent swipeEvent;
9598
MotionTriggerUpdateEvent strokeEvent;
96-
bool axisChanged{};
9799

98100
// Block the event even if the result says not to do so.
99101
bool block{};
@@ -161,57 +163,50 @@ bool MotionTriggerHandler::handleMotion(const InputDevice *device, const PointDe
161163
// TODO: Cancel if motion is a straight line
162164
}
163165

164-
if (hasActiveTriggers(TriggerType::Swipe)) {
165-
if (m_deltas.size() < 2) {
166-
// One delta may not be enough to determine the direction
167-
return true;
168-
}
166+
if (hasSwipe) {
167+
const auto motionThreshold = currentMotionThreshold(device);
168+
bool motionThresholdReached{};
169169

170-
if (m_currentSwipeAxis == Axis::None) {
171-
m_currentSwipeAxis = std::abs(m_totalSwipeDelta.x()) >= std::abs(m_totalSwipeDelta.y()) ? Axis::Horizontal : Axis::Vertical;
172-
} else if (m_deltas.size() >= AXIS_CHANGE_MIN_DELTA_COUNT) { // Make sure we have enough data to detect axis change
173-
const std::vector<QPointF> lastDeltas(m_deltas.end() - AXIS_CHANGE_MIN_DELTA_COUNT, m_deltas.end());
174-
QPointF sum;
175-
for (const auto &delta : lastDeltas) {
176-
sum += {std::abs(delta.x()), std::abs(delta.y())};
170+
QPointF totalDelta;
171+
auto it = m_swipeDeltas.begin();
172+
for (; it != m_swipeDeltas.end(); ++it) {
173+
totalDelta += *it;
174+
if (Math::hypot(totalDelta) < motionThreshold) {
175+
continue;
177176
}
178177

179-
if (std::min(sum.x(), sum.y()) / std::max(sum.x(), sum.y()) <= 0.2 // Must be a sharp turn
180-
&& ((m_currentSwipeAxis == Axis::Horizontal && sum.y() > sum.x()) || (m_currentSwipeAxis == Axis::Vertical && sum.x() > sum.y()))) {
181-
m_currentSwipeAxis = m_currentSwipeAxis == Axis::Horizontal ? Axis::Vertical : Axis::Horizontal;
182-
axisChanged = true;
183-
qCDebug(INPUTACTIONS_HANDLER_MOTION, "Swipe axis changed");
184-
}
178+
motionThresholdReached = true;
179+
break;
185180
}
186181

187-
SwipeDirection direction{};
188-
switch (m_currentSwipeAxis) {
189-
case Axis::Vertical:
190-
direction = m_totalSwipeDelta.y() < 0 ? SwipeDirection::Up : SwipeDirection::Down;
191-
break;
192-
case Axis::Horizontal:
193-
direction = m_totalSwipeDelta.x() < 0 ? SwipeDirection::Left : SwipeDirection::Right;
194-
break;
195-
default:
196-
Q_UNREACHABLE();
182+
if (!motionThresholdReached) {
183+
return hasActiveBlockingTriggers(TriggerType::Swipe);
197184
}
185+
m_swipeDeltas.erase(++it, m_swipeDeltas.end());
186+
187+
// Up should be 90°, not 270°
188+
auto currentDelta = delta.unaccelerated();
189+
currentDelta.setY(-currentDelta.y());
190+
totalDelta.setY(-totalDelta.y());
198191

199-
swipeEvent.setDelta(m_currentSwipeAxis == Axis::Vertical ? Delta(delta.accelerated().y(), delta.unaccelerated().y())
200-
: Delta(delta.accelerated().x(), delta.unaccelerated().x()));
201-
swipeEvent.setDirection(static_cast<TriggerDirection>(direction));
192+
swipeEvent.setAngle(Math::atan2deg360(currentDelta));
193+
swipeEvent.setAverageAngle(Math::atan2deg360(totalDelta / m_swipeDeltas.size()));
194+
swipeEvent.setDelta(Delta(delta.acceleratedHypot(), delta.unacceleratedHypot()));
202195
swipeEvent.setPointDelta({delta.accelerated() * m_swipeDeltaMultiplier, delta.unaccelerated() * m_swipeDeltaMultiplier});
203196
swipeEvent.setSpeed(speed);
204197
events[TriggerType::Swipe] = &swipeEvent;
205198
}
206199

207-
if (hasActiveTriggers(TriggerType::Stroke)) {
200+
if (hasStroke) {
208201
strokeEvent.setDelta(device->type() == InputDeviceType::Mouse ? delta.acceleratedHypot() : delta.unacceleratedHypot()); // backwards compatibility
209202
strokeEvent.setSpeed(speed);
210203
events[TriggerType::Stroke] = &strokeEvent;
211204
}
212205

213206
const auto result = updateTriggers(events);
214-
if (axisChanged && !result.success) {
207+
if (result.success) {
208+
m_swipeUpdates++;
209+
} else if (hasSwipe && m_swipeUpdates > 0) {
215210
activateTriggers(TriggerType::Swipe);
216211
return handleMotion(device, delta);
217212
}
@@ -256,18 +251,23 @@ bool MotionTriggerHandler::determineSpeed(TriggerType type, qreal delta, Trigger
256251
return true;
257252
}
258253

254+
qreal MotionTriggerHandler::currentMotionThreshold(const InputDevice *device) const
255+
{
256+
return device->properties().motionThreshold();
257+
}
258+
259259
void MotionTriggerHandler::reset()
260260
{
261261
TriggerHandler::reset();
262-
m_currentSwipeAxis = Axis::None;
263-
m_totalSwipeDelta = {};
264262
m_speed = {};
265263
m_isDeterminingSpeed = false;
266264
m_circleIsFirstEvent = true;
267265
m_deltas.clear();
268266
m_sampledInputEvents = m_accumulatedAbsoluteSampledDelta = m_circlePreviousAngle = m_circlePreviousDistance = m_circleFilterDelta = m_circleAdaptiveDelta
269267
= m_circleTotalDelta = 0;
270268
m_circleCoastingTimer.stop();
269+
m_swipeDeltas.clear();
270+
m_swipeUpdates = 0;
271271
}
272272

273273
void MotionTriggerHandler::onCircleCoastingTimerTick()

src/libinputactions/handlers/MotionTriggerHandler.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class MotionTriggerHandler : public InputTriggerHandler
8282
*/
8383
bool determineSpeed(TriggerType type, qreal delta, TriggerSpeed &speed, TriggerDirection direction = UINT32_MAX);
8484

85+
virtual qreal currentMotionThreshold(const InputDevice *device) const;
86+
8587
void reset() override;
8688

8789
private slots:
@@ -91,8 +93,11 @@ private slots:
9193
void onEndingTriggers(TriggerTypes types);
9294

9395
private:
94-
Axis m_currentSwipeAxis = Axis::None;
95-
QPointF m_totalSwipeDelta;
96+
/**
97+
* Contains latest events whose sum is roughly equal to (but never less than) the motion threshold, any events over the threshold are discarded.
98+
*/
99+
std::vector<QPointF> m_swipeDeltas;
100+
uint32_t m_swipeUpdates{};
96101

97102
bool m_isDeterminingSpeed = false;
98103
uint8_t m_sampledInputEvents = 0;
@@ -114,6 +119,7 @@ private slots:
114119
uint8_t m_inputEventsToSample = 3;
115120

116121
friend class MockTouchpadTriggerHandler;
122+
friend class TestMotionTriggerHandler;
117123
};
118124

119125
}

src/libinputactions/handlers/TouchpadTriggerHandler.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ TouchpadTriggerHandler::TouchpadTriggerHandler(InputDevice *device)
4444

4545
bool TouchpadTriggerHandler::pointerAxis(const MotionEvent &event)
4646
{
47+
m_libinputFingers = 2;
4748
bool isFirstEvent{};
4849
switch (m_state) {
4950
case State::Motion:
@@ -141,6 +142,7 @@ bool TouchpadTriggerHandler::pointerButton(const PointerButtonEvent &event)
141142

142143
bool TouchpadTriggerHandler::pointerMotion(const MotionEvent &event)
143144
{
145+
m_libinputFingers = 1;
144146
switch (m_state) {
145147
case State::Motion:
146148
case State::None:
@@ -253,6 +255,7 @@ bool TouchpadTriggerHandler::touchpadGestureLifecyclePhase(const TouchpadGesture
253255
switch (event.phase()) {
254256
case TouchpadGestureLifecyclePhase::Begin: {
255257
g_variableManager->getVariable(BuiltinVariables::Fingers)->set(event.fingers());
258+
m_libinputFingers = event.fingers();
256259

257260
// 1- and 2-finger hold gestures have almost no delay and are used to stop kinetic scrolling, there's no reason to block them
258261
m_gestureBeginBlocked = !(event.triggers() & TriggerType::Press && event.fingers() <= 2);
@@ -299,6 +302,17 @@ bool TouchpadTriggerHandler::touchpadSwipe(const MotionEvent &event)
299302
return handleMotion(event.sender(), event.delta());
300303
}
301304

305+
qreal TouchpadTriggerHandler::currentMotionThreshold(const InputDevice *device) const
306+
{
307+
const auto &properties = device->properties();
308+
if (m_libinputFingers == 1) {
309+
return properties.motionThreshold();
310+
} else if (m_libinputFingers == 2) {
311+
return properties.touchpadMotionThreshold2();
312+
}
313+
return properties.touchpadMotionThreshold3();
314+
}
315+
302316
bool TouchpadTriggerHandler::canTap()
303317
{
304318
return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_firstTouchPoint.downTimestamp).count()

src/libinputactions/handlers/TouchpadTriggerHandler.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class TouchpadTriggerHandler : public MultiTouchMotionTriggerHandler
5454
bool touchpadPinch(const TouchpadPinchEvent &event) override;
5555
bool touchpadSwipe(const MotionEvent &event) override;
5656

57+
qreal currentMotionThreshold(const InputDevice *device) const override;
58+
5759
private slots:
5860
void onLibinputTapTimeout();
5961

@@ -70,6 +72,10 @@ private slots:
7072
PointDelta m_pointerAxisDelta;
7173

7274
TouchPoint m_firstTouchPoint;
75+
/**
76+
* Finger count derived from libinput events, not evdev.
77+
*/
78+
uint8_t m_libinputFingers{};
7379

7480
enum State
7581
{

src/libinputactions/handlers/TriggerHandler.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class TriggerHandler : public QObject
5959
/**
6060
* Cancels all active triggers and activates triggers of the specified types eligible for activation.
6161
*/
62-
TriggerManagementOperationResult activateTriggers(TriggerTypes types, const TriggerActivationEvent &event);
62+
TEST_VIRTUAL TriggerManagementOperationResult activateTriggers(TriggerTypes types, const TriggerActivationEvent &event);
6363
/**
6464
* @see activateTriggers(const TriggerTypes &, const TriggerActivationEvent *)
6565
*/
@@ -68,7 +68,7 @@ class TriggerHandler : public QObject
6868
/**
6969
* Updates triggers of multiple types in order as added to the handler.
7070
*/
71-
TriggerManagementOperationResult updateTriggers(const std::map<TriggerType, const TriggerUpdateEvent *> &events);
71+
TEST_VIRTUAL TriggerManagementOperationResult updateTriggers(const std::map<TriggerType, const TriggerUpdateEvent *> &events);
7272
/**
7373
* Updates triggers of a single type.
7474
* @warning Do not use this to update multiple trigger types, as it will prevent conflict resolution from working
@@ -106,11 +106,11 @@ class TriggerHandler : public QObject
106106
/**
107107
* @return Whether there are any triggers of the specified types.
108108
*/
109-
bool hasActiveTriggers(TriggerTypes types = TriggerType::All);
109+
TEST_VIRTUAL bool hasActiveTriggers(TriggerTypes types = TriggerType::All);
110110
/**
111111
* @return Whether there are any blocking triggers of the specified types.
112112
*/
113-
bool hasActiveBlockingTriggers(TriggerTypes types = TriggerType::All);
113+
TEST_VIRTUAL bool hasActiveBlockingTriggers(TriggerTypes types = TriggerType::All);
114114

115115
/**
116116
* Creates a trigger activation event with information that can be provided by the input device(s).

src/libinputactions/helpers/Math.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ qreal atan2deg(const QPointF &point)
3131
return radToDeg(atan2(point));
3232
}
3333

34+
qreal atan2deg360(const QPointF &point)
35+
{
36+
const auto angle = atan2deg(point);
37+
if (angle < 0) {
38+
return angle + 360;
39+
}
40+
return angle;
41+
}
42+
3443
qreal radToDeg(qreal rad)
3544
{
3645
return 180.0 / M_PI * rad;

src/libinputactions/helpers/Math.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ namespace InputActions::Math
2525

2626
qreal atan2(const QPointF &point);
2727
/**
28-
* @return [0°, 180°]
28+
* @return [-180°, 180°]
2929
*/
3030
qreal atan2deg(const QPointF &point);
31+
/**
32+
* @return [0°, 360°]
33+
*/
34+
qreal atan2deg360(const QPointF &point);
3135
qreal radToDeg(qreal rad);
3236

3337
qreal hypot(const QPointF &point);

0 commit comments

Comments
 (0)