Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ set(libinputactions_SRCS
libinputactions/actions/CommandAction.cpp
libinputactions/actions/ContextMenuAction.cpp
libinputactions/actions/CustomAction.cpp
libinputactions/actions/DirectionalAction.cpp
libinputactions/actions/InputAction.cpp
libinputactions/actions/PlasmaGlobalShortcutAction.cpp
libinputactions/actions/ReplaceTextAction.cpp
Expand Down
5 changes: 5 additions & 0 deletions src/libinputactions/actions/Action.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <QString>
#include <libinputactions/PointF.h>
#include <memory>
#include <optional>

namespace InputActions
{
Expand All @@ -40,6 +41,10 @@ struct ActionExecutionArguments
* For the move_by_delta mouse action.
*/
PointF motionPointDelta;
/**
* Average angle of swipe triggers in degrees (right = 0, up = 90, left = 180, down = 270). For the directional action.
*/
std::optional<qreal> motionAngle;
} inputActionArgs;
};

Expand Down
167 changes: 167 additions & 0 deletions src/libinputactions/actions/DirectionalAction.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
Input Actions - Input handler that executes user-defined actions
Copyright (C) 2026 Owen271

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 3 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 <https://www.gnu.org/licenses/>.
*/

#include "DirectionalAction.h"
#include "ActionExecutor.h"
#include <QCoreApplication>
#include <QThread>
#include <algorithm>
#include <cmath>
#include <libinputactions/conditions/Condition.h>
#include <libinputactions/helpers/QThread.h>
#include <numbers>

namespace InputActions
{

/**
* Limits the actions executed for a single large motion event.
*/
static const int MAX_ACTIONS_PER_UPDATE = 8;

/**
* @return Index into Bindings for the dominant axis.
*/
static size_t direction(qreal x, qreal y)
{
if (std::abs(x) >= std::abs(y)) {
return x < 0 ? 0 : 1;
}
return y < 0 ? 2 : 3;
}

/**
* Motion against the accumulated direction restarts accumulation.
*/
static void accumulate(qreal &total, qreal delta)
{
total = delta != 0 && total * delta < 0 ? delta : total + delta;
}

DirectionalAction::DirectionalAction(qreal distance, Bindings actions, Bindings initial)
: m_distance(distance)
, m_actions(std::move(actions))
, m_initial(std::move(initial))
{
}

void DirectionalAction::reset()
{
Action::reset();
m_firstUpdate = true;
m_x = m_y = 0;
for (const auto *bindings : {&m_actions, &m_initial}) {
for (const auto &action : *bindings) {
if (action) {
action->reset();
}
}
}
}

void DirectionalAction::doExecute(const ActionExecutionArguments &args)
{
if (QThread::currentThread() == QThreadHelpers::mainThread()) {
move(args);
return;
}

// The repeat condition must be checked when the actions are executed, otherwise input could reach an application after the controlled UI has closed.
// Not blocking, as the main thread may be waiting for this thread, for example while reloading the configuration.
QMetaObject::invokeMethod(
QCoreApplication::instance(),
[this, alive = std::weak_ptr(m_alive), args] {
// The action may have been destroyed while the motion was queued.
if (alive.lock()) {
move(args);
}
},
Qt::QueuedConnection);
}

void DirectionalAction::move(const ActionExecutionArguments &args)
{
const auto delta = args.inputActionArgs.motionPointDelta;

// The initial action waits for motion. Only swipe triggers provide two-axis motion.
if (m_firstUpdate && delta.x() == 0 && delta.y() == 0) {
return;
}

const auto repeatAllowed = !m_repeatCondition || m_repeatCondition->satisfied();
if (m_firstUpdate) {
m_firstUpdate = false;
const auto hasInitial = std::ranges::any_of(m_initial, [](const auto &action) {
return action != nullptr;
});
// If the controlled UI is already open, navigate it instead of opening it again.
if (hasInitial && !(m_repeatCondition && repeatAllowed)) {
auto initialDirection = direction(delta.x(), delta.y());
if (args.inputActionArgs.motionAngle) {
const auto angle = *args.inputActionArgs.motionAngle * std::numbers::pi / 180;
initialDirection = direction(std::cos(angle), -std::sin(angle));
}
executeAction(m_initial[initialDirection].get(), args);
// The motion that executed the initial action does not count towards the next one.
return;
}
}

if (!repeatAllowed) {
m_x = m_y = 0;
return;
}

accumulate(m_x, delta.x());
accumulate(m_y, delta.y());
for (auto executed = 0; executed < MAX_ACTIONS_PER_UPDATE && std::max(std::abs(m_x), std::abs(m_y)) >= m_distance; executed++) {
// The previous action may have closed the controlled UI.
if (executed && m_repeatCondition && !m_repeatCondition->satisfied()) {
m_x = m_y = 0;
return;
}

const auto selected = direction(m_x, m_y);
executeAction(m_actions[selected].get(), args);
if (selected <= 1) {
m_x += m_x < 0 ? m_distance : -m_distance;
m_y = 0;
} else {
m_y += m_y < 0 ? m_distance : -m_distance;
m_x = 0;
}
}
}

void DirectionalAction::executeAction(Action *action, const ActionExecutionArguments &args)
{
if (!action || !action->canExecute()) {
return;
}
g_actionExecutor->execute(*action,
{
// Synchronous actions are executed immediately after the repeat condition has been checked. Asynchronous actions would
// block the main thread.
.thread = action->async() ? ActionThread::Auto : ActionThread::Current,
.actionArgs = {
.inputActionArgs = args.inputActionArgs,
},
});
}

}
86 changes: 86 additions & 0 deletions src/libinputactions/actions/DirectionalAction.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
Input Actions - Input handler that executes user-defined actions
Copyright (C) 2026 Owen271

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 3 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 <https://www.gnu.org/licenses/>.
*/

#pragma once

#include "Action.h"
#include <array>

namespace InputActions
{

class Condition;

/**
* Converts two-axis motion into discrete actions. Every time the motion along the dominant axis reaches the distance, the action for that direction is
* executed.
*/
class DirectionalAction : public Action
{
public:
/**
* Actions ordered left, right, up, down. Each may be nullptr.
*/
using Bindings = std::array<std::unique_ptr<Action>, 4>;

/**
* @param distance Motion required per action. Must be greater than zero.
* @param initial Executed on the first update with motion instead of waiting for the distance, using the swipe angle if available.
*/
DirectionalAction(qreal distance, Bindings actions, Bindings initial = {});

qreal distance() const { return m_distance; }
const Bindings &actions() const { return m_actions; }
const Bindings &initial() const { return m_initial; }

/**
* Checked before executing each action. While not satisfied, motion is discarded. If satisfied on the first update with motion, the initial action is not
* executed.
* May be nullptr.
*/
const std::shared_ptr<Condition> &repeatCondition() const { return m_repeatCondition; }
void setRepeatCondition(std::shared_ptr<Condition> value) { m_repeatCondition = std::move(value); }

void reset() override;

protected:
void doExecute(const ActionExecutionArguments &args) override;

private:
void move(const ActionExecutionArguments &args);
/**
* Must be called from the main thread.
*/
static void executeAction(Action *action, const ActionExecutionArguments &args);

qreal m_distance;
Bindings m_actions;
Bindings m_initial;
std::shared_ptr<Condition> m_repeatCondition;

/**
* Expires when the action is destroyed, for motion queued on the main thread.
*/
std::shared_ptr<bool> m_alive = std::make_shared<bool>();

bool m_firstUpdate = true;
qreal m_x{};
qreal m_y{};
};

}
5 changes: 5 additions & 0 deletions src/libinputactions/actions/TriggerAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "ActionExecutor.h"
#include <libinputactions/input/Delta.h>
#include <libinputactions/triggers/core/MotionTriggerCore.h>
#include <libinputactions/triggers/core/SwipeTriggerCore.h>

namespace InputActions
{
Expand Down Expand Up @@ -54,6 +55,9 @@ void TriggerAction::triggerUpdated(const TriggerUpdateEvent &event)
const auto &pointDelta = motionEvent->pointDelta();
m_currentMotionPointDelta = m_accelerated ? pointDelta.accelerated() : pointDelta.unaccelerated();
}
if (const auto *swipeEvent = dynamic_cast<const SwipeTriggerUpdateEvent *>(&event)) {
m_currentMotionAngle = swipeEvent->averageAngle();
}
update(event.delta());
}

Expand Down Expand Up @@ -88,6 +92,7 @@ void TriggerAction::tryExecute(uint32_t executions)
.executions = executions,
.inputActionArgs = {
.motionPointDelta = m_currentMotionPointDelta,
.motionAngle = m_currentMotionAngle,
},
}});
}
Expand Down
1 change: 1 addition & 0 deletions src/libinputactions/actions/TriggerAction.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class TriggerAction
qreal m_absoluteAccumulatedDelta{};

PointF m_currentMotionPointDelta;
std::optional<qreal> m_currentMotionAngle;

friend class TestTrigger;
};
Expand Down
Loading