diff --git a/src/libinputactions/config/parsers/triggers.cpp b/src/libinputactions/config/parsers/triggers.cpp index 616c577..164bd54 100644 --- a/src/libinputactions/config/parsers/triggers.cpp +++ b/src/libinputactions/config/parsers/triggers.cpp @@ -104,6 +104,7 @@ void NodeParser>::parse(const Node *node, std: loadSetter(swipeTriggerCore.get(), &SwipeTriggerCore::setBidirectional, node->at("bidirectional")); result = std::move(swipeTriggerCore); } + loadSetter(result.get(), &SwipeTriggerCore::setLockDirection, node->at("lock_direction")); } void finalizeTrigger(const Node *node, Trigger *trigger) diff --git a/src/libinputactions/triggers/Trigger.cpp b/src/libinputactions/triggers/Trigger.cpp index 17c4f3e..adeebe3 100644 --- a/src/libinputactions/triggers/Trigger.cpp +++ b/src/libinputactions/triggers/Trigger.cpp @@ -54,7 +54,7 @@ bool Trigger::canActivate(const TriggerActivationEvent &event) const bool Trigger::canUpdate(const TriggerUpdateEvent &event) const { - return m_core->canUpdate(event); + return m_updatedDuringGesture ? m_core->canContinue(event) : m_core->canUpdate(event); } bool Trigger::endIfCannotUpdate() const @@ -84,6 +84,7 @@ void Trigger::update(const TriggerUpdateEvent &event) } } + m_updatedDuringGesture = true; setLastTrigger(); m_core->updateActions(m_actions, event); } @@ -101,6 +102,7 @@ void Trigger::end(bool allowResuming) } if (allowResuming && m_resumeTimeout.count()) { + m_updatedDuringGesture = false; m_resumeTimeoutTimer.start(m_resumeTimeout.count()); return; } @@ -198,6 +200,7 @@ void Trigger::setLastTrigger() void Trigger::reset() { m_started = false; + m_updatedDuringGesture = false; m_absoluteAccumulatedDelta = 0; m_withinThreshold = false; m_tickTimer.stop(); diff --git a/src/libinputactions/triggers/Trigger.h b/src/libinputactions/triggers/Trigger.h index 6713095..e7fc897 100644 --- a/src/libinputactions/triggers/Trigger.h +++ b/src/libinputactions/triggers/Trigger.h @@ -198,6 +198,11 @@ private slots: std::vector> m_actions; bool m_started = false; + /** + * Whether the trigger has been updated during the current gesture. Unlike m_started, cleared while waiting for the gesture to be resumed, so that the + * resumed gesture is checked by canUpdate as if it were new. + */ + bool m_updatedDuringGesture = false; QTimer m_tickTimer; bool m_withinThreshold = false; diff --git a/src/libinputactions/triggers/core/SwipeTriggerCore.cpp b/src/libinputactions/triggers/core/SwipeTriggerCore.cpp index 6238bff..fd5fde3 100644 --- a/src/libinputactions/triggers/core/SwipeTriggerCore.cpp +++ b/src/libinputactions/triggers/core/SwipeTriggerCore.cpp @@ -56,6 +56,11 @@ bool SwipeTriggerCore::canUpdate(const TriggerUpdateEvent &event) const return matchesAngleRange(angle, range.min, range.max) || (m_bidirectional && matchesOppositeAngleRange(angle, range.min, range.max)); } +bool SwipeTriggerCore::canContinue(const TriggerUpdateEvent &event) const +{ + return m_lockDirection ? MotionTriggerCore::canUpdate(event) : canUpdate(event); +} + void SwipeTriggerCore::updateActions(const std::vector> &actions, const TriggerUpdateEvent &event) const { auto newEvent = dynamic_cast(event); diff --git a/src/libinputactions/triggers/core/SwipeTriggerCore.h b/src/libinputactions/triggers/core/SwipeTriggerCore.h index 2f99918..42a1376 100644 --- a/src/libinputactions/triggers/core/SwipeTriggerCore.h +++ b/src/libinputactions/triggers/core/SwipeTriggerCore.h @@ -93,7 +93,7 @@ enum class SwipeDirection * in DeviceProperties. Touchpads have 3 different thresholds, depending on the finger count. * * If swipe triggers are active and the motion angle changes, but none of the active triggers acccept it, they are cancelled and all swipe triggers are - * activated again, allowing for chaining multiple triggers together. + * activated again, allowing for chaining multiple triggers together. Started triggers with lockDirection set are not cancelled by angle changes. */ class SwipeTriggerCore : public MotionTriggerCore { @@ -115,7 +115,15 @@ class SwipeTriggerCore : public MotionTriggerCore bool bidirectional() const { return m_bidirectional; } void setBidirectional(bool value) { m_bidirectional = value; } + /** + * Whether the angle is only checked until the trigger starts. A started trigger then stays active regardless of direction changes, preventing chaining. + * Other conditions, such as speed, are still checked. A resumed gesture must match the angle again. + */ + bool lockDirection() const { return m_lockDirection; } + void setLockDirection(bool value) { m_lockDirection = value; } + bool canUpdate(const TriggerUpdateEvent &event) const override; + bool canContinue(const TriggerUpdateEvent &event) const override; void updateActions(const std::vector> &actions, const TriggerUpdateEvent &event) const override; private: @@ -139,6 +147,7 @@ class SwipeTriggerCore : public MotionTriggerCore qreal m_minAngle{}; qreal m_maxAngle{}; bool m_bidirectional{}; + bool m_lockDirection{}; friend class TestSwipeTriggerCore; }; diff --git a/src/libinputactions/triggers/core/TriggerCore.cpp b/src/libinputactions/triggers/core/TriggerCore.cpp index ce3877e..2e43391 100644 --- a/src/libinputactions/triggers/core/TriggerCore.cpp +++ b/src/libinputactions/triggers/core/TriggerCore.cpp @@ -28,6 +28,11 @@ bool TriggerCore::canUpdate(const TriggerUpdateEvent &event) const return true; } +bool TriggerCore::canContinue(const TriggerUpdateEvent &event) const +{ + return canUpdate(event); +} + void TriggerCore::updateActions(const std::vector> &actions, const TriggerUpdateEvent &event) const { doUpdateActions(actions, event); diff --git a/src/libinputactions/triggers/core/TriggerCore.h b/src/libinputactions/triggers/core/TriggerCore.h index d0f4a22..d4abfde 100644 --- a/src/libinputactions/triggers/core/TriggerCore.h +++ b/src/libinputactions/triggers/core/TriggerCore.h @@ -37,6 +37,10 @@ class TriggerCore virtual ~TriggerCore() = default; virtual bool canUpdate(const TriggerUpdateEvent &event) const; + /** + * Called instead of canUpdate once the trigger has been updated during the current gesture. A resumed gesture is checked by canUpdate again. + */ + virtual bool canContinue(const TriggerUpdateEvent &event) const; /** * Override to modify the event before passing it to actions. Must be called by the overriding method. */ diff --git a/tests/libinputactions/config/parsers/triggers/TestSwipeTriggerCoreNodeParser.cpp b/tests/libinputactions/config/parsers/triggers/TestSwipeTriggerCoreNodeParser.cpp index 30579d6..3603887 100644 --- a/tests/libinputactions/config/parsers/triggers/TestSwipeTriggerCoreNodeParser.cpp +++ b/tests/libinputactions/config/parsers/triggers/TestSwipeTriggerCoreNodeParser.cpp @@ -19,6 +19,18 @@ private slots: QCOMPARE(core->minAngle(), 30); QCOMPARE(core->maxAngle(), 60); QCOMPARE(core->bidirectional(), false); + QCOMPARE(core->lockDirection(), false); + } + + void angle_lockDirection__parsesNodeCorrectly() + { + const auto node = Node::create(R"( + angle: 30-60 + lock_direction: true + )"); + + const auto core = node->as>(); + QCOMPARE(core->lockDirection(), true); } void angle_bidirectional__parsesNodeCorrectly() diff --git a/tests/libinputactions/handlers/TestMotionTriggerHandler.cpp b/tests/libinputactions/handlers/TestMotionTriggerHandler.cpp index ae3b074..18406f8 100644 --- a/tests/libinputactions/handlers/TestMotionTriggerHandler.cpp +++ b/tests/libinputactions/handlers/TestMotionTriggerHandler.cpp @@ -1,5 +1,7 @@ #include "Test.h" #include "mocks/MockMotionTriggerHandler.h" +#include +#include #include #include #include @@ -20,6 +22,76 @@ private slots: m_handler = std::make_unique(); m_mouse = std::make_unique(InputDeviceType::Mouse); m_mouse->properties().setMotionThreshold(10); + m_rightBegins = 0; + m_leftBegins = 0; + } + + void handleMotion_swipe_directionChanged__chainsUnlessLocked_data() + { + QTest::addColumn("lockDirection"); + QTest::addColumn("rightBegins"); + QTest::addColumn("leftBegins"); + + QTest::newRow("not locked") << false << 2u << 1u; + QTest::newRow("locked") << true << 1u << 0u; + } + + void handleMotion_swipe_directionChanged__chainsUnlessLocked() + { + QFETCH(bool, lockDirection); + QFETCH(uint32_t, rightBegins); + QFETCH(uint32_t, leftBegins); + + addSwipeTrigger(SwipeDirection::Right, lockDirection, m_rightBegins); + addSwipeTrigger(SwipeDirection::Left, false, m_leftBegins); + + m_handler->activateTriggers(TriggerType::Swipe, TriggerActivationEvent{}); + m_handler->handleMotion(m_mouse.get(), PointDelta({20, 0})); + m_handler->handleMotion(m_mouse.get(), PointDelta({-20, 0})); + m_handler->handleMotion(m_mouse.get(), PointDelta({20, 0})); + + QCOMPARE(m_rightBegins, rightBegins); + QCOMPARE(m_leftBegins, leftBegins); + m_handler->cancelTriggers(TriggerType::Swipe); + } + + void handleMotion_swipe_lockDirection_belowThreshold_directionChanged__activatesMatchingTrigger() + { + addSwipeTrigger(SwipeDirection::Right, true, m_rightBegins, {}, Range(30, {})); + addSwipeTrigger(SwipeDirection::Left, false, m_leftBegins, {}, Range(30, {})); + + m_handler->activateTriggers(TriggerType::Swipe, TriggerActivationEvent{}); + m_handler->handleMotion(m_mouse.get(), PointDelta({20, 0})); + m_handler->handleMotion(m_mouse.get(), PointDelta({-40, 0})); + + QCOMPARE(m_rightBegins, 0u); + QCOMPARE(m_leftBegins, 1u); + m_handler->cancelTriggers(TriggerType::Swipe); + } + + void handleMotion_swipe_lockDirection_nextGestureInOppositeDirection__activatesOtherTrigger_data() + { + QTest::addColumn("resumeTimeout"); + + QTest::newRow("new gesture") << std::chrono::milliseconds(0); + QTest::newRow("resumed gesture") << std::chrono::milliseconds(30000); + } + + void handleMotion_swipe_lockDirection_nextGestureInOppositeDirection__activatesOtherTrigger() + { + QFETCH(std::chrono::milliseconds, resumeTimeout); + addSwipeTrigger(SwipeDirection::Right, true, m_rightBegins, resumeTimeout); + addSwipeTrigger(SwipeDirection::Left, false, m_leftBegins); + + m_handler->activateTriggers(TriggerType::Swipe, TriggerActivationEvent{}); + m_handler->handleMotion(m_mouse.get(), PointDelta({20, 0})); + m_handler->endTriggers(TriggerType::Swipe); + m_handler->activateTriggers(TriggerType::Swipe, TriggerActivationEvent{}); + m_handler->handleMotion(m_mouse.get(), PointDelta({-20, 0})); + + QCOMPARE(m_rightBegins, 1u); + QCOMPARE(m_leftBegins, 1u); + m_handler->cancelTriggers(TriggerType::Swipe); } void handleMotion_swipe__calculatesAnglesCorrectly_data() @@ -153,8 +225,32 @@ private slots: } private: + /** + * Adds a swipe trigger whose begin action increments the counter. + */ + void addSwipeTrigger(SwipeDirection direction, bool lockDirection, uint32_t &begins, std::chrono::milliseconds resumeTimeout = {}, + std::optional> threshold = {}) + { + auto core = std::make_unique(direction); + core->setLockDirection(lockDirection); + auto trigger = std::make_unique(TriggerType::Swipe, std::move(core)); + trigger->setResumeTimeout(resumeTimeout); + if (threshold) { + trigger->setThreshold(*threshold); + } + + auto action = std::make_unique(std::make_unique([&begins](const auto &) { + begins++; + })); + action->setOn(On::Begin); + trigger->addAction(std::move(action)); + m_handler->addTrigger(std::move(trigger)); + } + std::unique_ptr m_handler; std::unique_ptr m_mouse; + uint32_t m_rightBegins{}; + uint32_t m_leftBegins{}; }; } diff --git a/tests/libinputactions/triggers/core/TestSwipeTriggerCore.cpp b/tests/libinputactions/triggers/core/TestSwipeTriggerCore.cpp index a90e64b..c130e62 100644 --- a/tests/libinputactions/triggers/core/TestSwipeTriggerCore.cpp +++ b/tests/libinputactions/triggers/core/TestSwipeTriggerCore.cpp @@ -11,6 +11,20 @@ class TestSwipeTriggerCore : public Test Q_OBJECT private slots: + + void canContinue_lockDirection_speedDoesNotMatch__returnsFalse() + { + SwipeTriggerCore core(SwipeDirection::Right); + core.setLockDirection(true); + core.setSpeed(TriggerSpeed::Fast); + + SwipeTriggerUpdateEvent event; + event.setAverageAngle(90); + event.setSpeed(TriggerSpeed::Slow); + + QCOMPARE(core.canContinue(event), false); + } + void canUpdate_currentAngleDoesNotMatchRange_averageAngleMatchesRange__returnsTrue() { SwipeTriggerCore core(30, 60);