# validated: 2024-04-02 DS 0b1345946950 button/Trigger.java
from types import SimpleNamespace
from typing import Callable, overload
from typing_extensions import Self
from wpilib.event import EventLoop
from wpimath.filter import Debouncer
from ..command import Command
from ..commandscheduler import CommandScheduler
from ..util import format_args_kwargs
[docs]
class Trigger:
"""
This class provides an easy way to link commands to conditions.
It is very easy to link a button to a command. For instance, you could link the trigger button
of a joystick to a "score" command.
"""
_loop: EventLoop
_condition: Callable[[], bool]
@overload
def __init__(self, condition: Callable[[], bool] = lambda: False):
"""
Creates a new trigger based on the given condition.
Polled by the default scheduler button loop.
:param condition: the condition represented by this trigger
"""
...
@overload
def __init__(self, loop: EventLoop, condition: Callable[[], bool]):
"""
Creates a new trigger based on the given condition.
:param loop: The loop instance that polls this trigger.
:param condition: the condition represented by this trigger
"""
...
def __init__(self, *args, **kwargs):
def init_loop_condition(loop: EventLoop, condition: Callable[[], bool]):
assert callable(condition)
self._loop = loop
self._condition = condition
def init_condition(condition: Callable[[], bool]):
init_loop_condition(
CommandScheduler.getInstance().getDefaultButtonLoop(), condition
)
num_args = len(args) + len(kwargs)
if num_args == 0:
return init_condition(lambda: False)
elif num_args == 1 and len(kwargs) == 1:
if "condition" in kwargs:
return init_condition(kwargs["condition"])
elif num_args == 1 and len(args) == 1:
if callable(args[0]):
return init_condition(args[0])
elif num_args == 2:
loop, condition, *_ = args + (None, None)
if "loop" in kwargs:
loop = kwargs["loop"]
if "condition" in kwargs:
condition = kwargs["condition"]
if loop is not None and condition is not None:
return init_loop_condition(loop, condition)
raise TypeError(
f"""
TypeError: Trigger(): incompatible function arguments. The following argument types are supported:
1. (self: Trigger)
2. (self: Trigger, condition: () -> bool)
3. (self: Trigger, loop: EventLoop, condition: () -> bool)
Invoked with: {format_args_kwargs(self, *args, **kwargs)}
"""
)
[docs]
def onTrue(self, command: Command) -> Self:
"""
Starts the given command whenever the condition changes from `False` to `True`.
:param command: the command to start
:returns: this trigger, so calls can be chained
"""
state = SimpleNamespace(pressed_last=self._condition())
@self._loop.bind
def _():
pressed = self._condition()
if not state.pressed_last and pressed:
command.schedule()
state.pressed_last = pressed
return self
[docs]
def onFalse(self, command: Command) -> Self:
"""
Starts the given command whenever the condition changes from `True` to `False`.
:param command: the command to start
:returns: this trigger, so calls can be chained
"""
state = SimpleNamespace(pressed_last=self._condition())
@self._loop.bind
def _():
pressed = self._condition()
if state.pressed_last and not pressed:
command.schedule()
state.pressed_last = pressed
return self
[docs]
def whileTrue(self, command: Command) -> Self:
"""
Starts the given command when the condition changes to `True` and cancels it when the condition
changes to `False`.
Doesn't re-start the command if it ends while the condition is still `True`. If the command
should restart, see :class:`commands2.RepeatCommand`.
:param command: the command to start
:returns: this trigger, so calls can be chained
"""
state = SimpleNamespace(pressed_last=self._condition())
@self._loop.bind
def _():
pressed = self._condition()
if not state.pressed_last and pressed:
command.schedule()
elif state.pressed_last and not pressed:
command.cancel()
state.pressed_last = pressed
return self
[docs]
def whileFalse(self, command: Command) -> Self:
"""
Starts the given command when the condition changes to `False` and cancels it when the
condition changes to `True`.
Doesn't re-start the command if it ends while the condition is still `False`. If the command
should restart, see :class:`commands2.RepeatCommand`.
:param command: the command to start
:returns: this trigger, so calls can be chained
"""
state = SimpleNamespace(pressed_last=self._condition())
@self._loop.bind
def _():
pressed = self._condition()
if state.pressed_last and not pressed:
command.schedule()
elif not state.pressed_last and pressed:
command.cancel()
state.pressed_last = pressed
return self
[docs]
def toggleOnTrue(self, command: Command) -> Self:
"""
Toggles a command when the condition changes from `False` to `True`.
:param command: the command to toggle
:returns: this trigger, so calls can be chained
"""
state = SimpleNamespace(pressed_last=self._condition())
@self._loop.bind
def _():
pressed = self._condition()
if not state.pressed_last and pressed:
if command.isScheduled():
command.cancel()
else:
command.schedule()
state.pressed_last = pressed
return self
[docs]
def toggleOnFalse(self, command: Command) -> Self:
"""
Toggles a command when the condition changes from `True` to `False`.
:param command: the command to toggle
:returns: this trigger, so calls can be chained
"""
state = SimpleNamespace(pressed_last=self._condition())
@self._loop.bind
def _():
pressed = self._condition()
if state.pressed_last and not pressed:
if command.isScheduled():
command.cancel()
else:
command.schedule()
state.pressed_last = pressed
return self
def __call__(self) -> bool:
return self._condition()
[docs]
def getAsBoolean(self) -> bool:
return self._condition()
def __bool__(self) -> bool:
return self._condition()
def __and__(self, other: Callable[[], bool]) -> "Trigger":
assert callable(other)
return Trigger(self._loop, lambda: self() and other())
[docs]
def and_(self, other: Callable[[], bool]) -> "Trigger":
"""
Composes two triggers with logical AND.
:param trigger: the condition to compose with
:returns: A trigger which is active when both component triggers are active.
"""
return self & other
def __or__(self, other: Callable[[], bool]) -> "Trigger":
assert callable(other)
return Trigger(self._loop, lambda: self() or other())
[docs]
def or_(self, other: Callable[[], bool]) -> "Trigger":
"""
Composes two triggers with logical OR.
:param trigger: the condition to compose with
:returns: A trigger which is active when either component trigger is active.
"""
return self | other
def __invert__(self) -> "Trigger":
return Trigger(self._loop, lambda: not self())
[docs]
def negate(self) -> "Trigger":
"""
Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the
negation of this trigger.
:returns: the negated trigger
"""
return ~self
[docs]
def not_(self) -> "Trigger":
return ~self
[docs]
def debounce(
self,
seconds: float,
debounce_type: Debouncer.DebounceType = Debouncer.DebounceType.kRising,
) -> "Trigger":
"""
Creates a new debounced trigger from this trigger - it will become active when this trigger has
been active for longer than the specified period.
:param seconds: The debounce period.
:param type: The debounce type.
:returns: The debounced trigger.
"""
debouncer = Debouncer(seconds, debounce_type)
return Trigger(self._loop, lambda: debouncer.calculate(self()))