Skip to content

Commit

Permalink
Add actions handler (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszsmolinski authored Jan 4, 2024
1 parent 6a195bf commit 433178d
Show file tree
Hide file tree
Showing 11 changed files with 603 additions and 53 deletions.
Empty file.
Empty file.
149 changes: 149 additions & 0 deletions midi_app_controller/actions/_tests/test_actions_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from unittest.mock import Mock, call

import pytest
from app_model.types import Action

from ..bound_controller import BoundController
from ..actions_handler import ActionsHandler
from midi_app_controller.models.binds import Binds
from midi_app_controller.models.controller import Controller


@pytest.fixture
def bound_controller() -> BoundController:
binds_data = {
"name": "TestBinds",
"app_name": "TestApp",
"controller_name": "TestController",
"button_binds": [{"button_id": 1, "action_id": "Action1"}],
"knob_binds": [
{
"knob_id": 2,
"action_id_increase": "incr",
"action_id_decrease": "decr",
}
],
}

controller_data = {
"name": "TestController",
"button_value_off": 0,
"button_value_on": 127,
"knob_value_min": 0,
"knob_value_max": 127,
"buttons": [{"id": 0, "name": "Button1"}, {"id": 1, "name": "Button2"}],
"knobs": [{"id": 2, "name": "Knob1"}, {"id": 3, "name": "Knob2"}],
}

actions = [
Action(
id="Action1",
title="sdfgsdfg",
callback=lambda: None,
),
Action(
id="incr",
title="dfgssdfg",
callback=lambda: None,
),
Action(
id="decr",
title="sdfdfg",
callback=lambda: None,
),
Action(
id="other",
title="aaaasfd",
callback=lambda: None,
),
]

return BoundController.create(
controller=Controller(**controller_data),
binds=Binds(**binds_data),
actions=actions,
)


def test_is_button_toggled(bound_controller):
actions_handler = ActionsHandler(bound_controller=bound_controller, app=Mock())

with pytest.raises(NotImplementedError):
actions_handler.is_button_toggled(0)


def test_get_knob_value(bound_controller):
actions_handler = ActionsHandler(bound_controller=bound_controller, app=Mock())

with pytest.raises(NotImplementedError):
actions_handler.get_knob_value(0)


def test_handle_button_action(bound_controller):
app = Mock()
actions_handler = ActionsHandler(bound_controller=bound_controller, app=app)

actions_handler.handle_button_action(1)

app.commands.execute_command.assert_called_once_with("Action1")


@pytest.mark.parametrize("button_id", [3, 10, 1000])
def test_handle_button_action_when_button_not_bound(bound_controller, button_id):
app = Mock()
actions_handler = ActionsHandler(bound_controller=bound_controller, app=app)

actions_handler.handle_button_action(button_id)

app.commands.execute_command.assert_not_called()


@pytest.mark.parametrize("old_value, new_value", [(10, 20), (10, 10), (10, 11)])
def test_handle_knob_action_increase(bound_controller, old_value, new_value):
app = Mock()
actions_handler = ActionsHandler(bound_controller=bound_controller, app=app)

actions_handler.handle_knob_action(
knob_id=2, old_value=old_value, new_value=new_value
)

assert old_value <= new_value
calls = [call("incr")] * (new_value - old_value)
app.commands.execute_command.assert_has_calls(calls)


@pytest.mark.parametrize("old_value, new_value", [(20, 10), (11, 10)])
def test_handle_knob_action_decrease(bound_controller, old_value, new_value):
app = Mock()
actions_handler = ActionsHandler(bound_controller=bound_controller, app=app)

actions_handler.handle_knob_action(
knob_id=2, old_value=old_value, new_value=new_value
)

assert old_value > new_value
calls = [call("decr")] * (old_value - new_value)
app.commands.execute_command.assert_has_calls(calls)


@pytest.mark.parametrize(
"knob_id, old_value, new_value",
[
(3, 10, 10),
(3, 11, 10),
(3, 10, 11),
(1000, 10, 20),
(-1, 10, 20),
],
)
def test_handle_knob_action_when_knob_not_bound(
bound_controller, knob_id, old_value, new_value
):
app = Mock()
actions_handler = ActionsHandler(bound_controller=bound_controller, app=app)

actions_handler.handle_knob_action(
knob_id=knob_id, old_value=old_value, new_value=new_value
)

app.commands.execute_command.assert_not_called()
162 changes: 162 additions & 0 deletions midi_app_controller/actions/_tests/test_bound_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import pytest
from typing import List
from app_model.types import Action

from ..bound_controller import BoundController, ButtonActions, KnobActions
from midi_app_controller.models.binds import Binds
from midi_app_controller.models.controller import Controller


@pytest.fixture
def binds() -> Binds:
binds_data = {
"name": "TestBinds",
"description": "Test description",
"app_name": "TestApp",
"controller_name": "TestController",
"button_binds": [{"button_id": 1, "action_id": "Action1"}],
"knob_binds": [
{
"knob_id": 2,
"action_id_increase": "incr",
"action_id_decrease": "decr",
}
],
}
return Binds(**binds_data)


@pytest.fixture
def controller() -> Controller:
controller_data = {
"name": "TestController",
"button_value_off": 11,
"button_value_on": 100,
"knob_value_min": 33,
"knob_value_max": 55,
"buttons": [{"id": 0, "name": "Button1"}, {"id": 1, "name": "Button2"}],
"knobs": [{"id": 2, "name": "Knob1"}, {"id": 3, "name": "Knob2"}],
}
return Controller(**controller_data)


@pytest.fixture
def actions() -> List[Action]:
return [
Action(
id="Action1",
title="sdfgsdfg",
callback=lambda: None,
),
Action(
id="incr",
title="dfgssdfg",
callback=lambda: None,
),
Action(
id="decr",
title="sdfdfg",
callback=lambda: None,
),
Action(
id="other",
title="aaaasfd",
callback=lambda: None,
),
]


@pytest.fixture
def bound_controller(binds, controller, actions) -> BoundController:
return BoundController.create(binds=binds, controller=controller, actions=actions)


def test_button_actions(actions):
data = {"action_press": actions[0]}
actions = ButtonActions(**data)

assert actions.dict() == data


def test_knob_actions(actions):
data = {"action_increase": actions[0], "action_decrease": actions[1]}
actions = KnobActions(**data)

assert actions.dict() == data


def test_bound_controller(binds, controller, actions):
bound_controller = BoundController.create(
binds=binds, controller=controller, actions=actions
)

assert bound_controller.knob_value_min == controller.knob_value_min
assert bound_controller.knob_value_max == controller.knob_value_max
assert bound_controller.buttons.keys() == {
bind.button_id for bind in binds.button_binds
}
assert bound_controller.knobs.keys() == {bind.knob_id for bind in binds.knob_binds}

for bind in binds.button_binds:
bound_button = bound_controller.buttons[bind.button_id]
assert bound_button.action_press.id == bind.action_id
for bind in binds.knob_binds:
bound_knob = bound_controller.knobs[bind.knob_id]
assert bound_knob.action_increase.id == bind.action_id_increase
assert bound_knob.action_decrease.id == bind.action_id_decrease


@pytest.mark.parametrize("action_index_to_delete", [0, 1, 2])
def test_non_existent_action_id(binds, controller, actions, action_index_to_delete):
actions.pop(action_index_to_delete)

with pytest.raises(ValueError):
BoundController.create(binds=binds, controller=controller, actions=actions)


def test_non_existent_knob(binds, controller, actions):
controller.knobs.pop(0)

with pytest.raises(ValueError):
BoundController.create(binds=binds, controller=controller, actions=actions)


def test_non_existent_button(binds, controller, actions):
controller.buttons.pop(1)

with pytest.raises(ValueError):
BoundController.create(binds=binds, controller=controller, actions=actions)


def test_different_controller_name(binds, controller, actions):
controller.name = "asdfjkasdfjk"

with pytest.raises(ValueError):
BoundController.create(binds=binds, controller=controller, actions=actions)


def test_get_button_press_action(actions, bound_controller):
assert bound_controller.get_button_press_action(1) == actions[0]


@pytest.mark.parametrize("knob_id", [2, 10, 1000])
def test_get_button_press_action_when_not_found(bound_controller, knob_id):
assert bound_controller.get_button_press_action(knob_id) is None


def test_get_knob_increase_action(actions, bound_controller):
assert bound_controller.get_knob_increase_action(2) == actions[1]


@pytest.mark.parametrize("knob_id", [3, 10, 1000])
def test_get_knob_increase_action_when_not_found(bound_controller, knob_id):
assert bound_controller.get_knob_increase_action(knob_id) is None


def test_get_knob_decrease_action(actions, bound_controller):
assert bound_controller.get_knob_decrease_action(2) == actions[2]


@pytest.mark.parametrize("knob_id", [3, 10, 1000])
def test_get_knob_decrease_action_when_not_found(bound_controller, knob_id):
assert bound_controller.get_knob_decrease_action(knob_id) is None
73 changes: 73 additions & 0 deletions midi_app_controller/actions/actions_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from app_model import Application
from typing import Optional

from .bound_controller import BoundController


class ActionsHandler:
"""Allows to execute actions and get their state using ids of controller elements."""

def __init__(
self,
*,
bound_controller: BoundController,
app: Application,
) -> None:
"""Initializes the handler.
Parameters
----------
bound_controller : BoundController
Controller and binds that should be handled.
app : Application
Application where the actions will be executed.
"""
self.bound_controller = bound_controller
self.app = app

def is_button_toggled(self, button_id: int) -> Optional[bool]:
"""Checks if the action associated with the button is toggled."""
raise NotImplementedError # TODO

def get_knob_value(self, knob_id: int) -> Optional[int]:
"""Returns knob's value from the action associated with the knob."""
raise NotImplementedError # TODO

def handle_button_action(self, button_id: int) -> None:
"""Executes an action associated with the button if it exists."""
action = self.bound_controller.get_button_press_action(button_id)
if action is not None:
self.app.commands.execute_command(action.id)

def handle_knob_action(
self,
*,
knob_id: int,
old_value: int,
new_value: int,
) -> None:
"""Executes an action based on how the knob's value changed if it exists.
Parameters
----------
knob_id : int
The id of the knob.
old_value : int
Value of the knob before it was rotated.
new_value : int
Received new value of the knob.
"""
diff = new_value - old_value
if diff >= 0:
action = self.bound_controller.get_knob_increase_action(knob_id)
else:
action = self.bound_controller.get_knob_decrease_action(knob_id)

# TODO How many times should the command be executed? Maybe we
# should let the user set it while binding the knob?
# When fetching current value of actions will be added to app_model,
# we could also execute the action once, check how much value
# changed, and then adjust the sensitivity accordingly.
if action is not None:
for _ in range(abs(diff)):
self.app.commands.execute_command(action.id)
Loading

0 comments on commit 433178d

Please # to comment.