Source code for simvx.core.input.events
"""Input binding and event types."""
from dataclasses import dataclass
from .enums import JoyAxis, JoyButton, Key, MouseButton, key_to_name, name_to_keys
MODIFIER_NAMES = ("ctrl", "shift", "alt")
[docs]
@dataclass
class InputBinding:
"""A single input binding: maps to a key, mouse button, or gamepad input.
``ctrl`` / ``shift`` / ``alt`` are REQUIRED modifiers, not exclusive ones: a
binding that sets ``shift=True`` only matches while Shift is held, while a
binding that leaves them all False matches whatever else is held. That keeps
every unmodified binding behaving as it always has (a jump bound to Space
still fires while the player holds Shift to run).
Key routing that has to tell one combo from another -- the UI navigation
actions, which must distinguish Tab from Shift+Tab -- compares
:attr:`key_combo` against the combo the router built for the event, so there
the match is exact.
"""
key: Key | None = None
mouse_button: MouseButton | None = None
joy_button: JoyButton | None = None
joy_axis: JoyAxis | None = None
joy_axis_positive: bool = True # For splitting an axis into two actions
deadzone: float = 0.2
ctrl: bool = False
shift: bool = False
alt: bool = False
[docs]
@property
def key_combo(self) -> str | None:
"""The combo spelling of a key binding (``"shift+tab"``), or None if not a key.
Modifier order is ctrl, shift, alt, matching the combo string the UI
router builds from the live modifier state.
"""
if self.key is None:
return None
parts = [name for name, held in (("ctrl", self.ctrl), ("shift", self.shift), ("alt", self.alt)) if held]
parts.append(key_to_name(self.key))
return "+".join(parts)
[docs]
def key_combo_to_binding(spec: str) -> InputBinding | None:
"""Parse a combo such as ``"shift+tab"`` into a binding; None when *spec* is not one.
The inverse of :attr:`InputBinding.key_combo`, and the one place that
spelling is understood: the input map, the project file and anything else
that accepts a written binding all resolve combos through here, so a combo
that an ``InputMap`` accepts is also one a ``simvx.toml`` can carry.
A bare key name is not a combo (there is nothing before the ``+``) and
returns None, leaving it to the caller's own key lookup.
"""
parts = spec.lower().split("+")
mods = set(parts[:-1])
if not mods or not mods.issubset(MODIFIER_NAMES):
return None
keys = name_to_keys(parts[-1])
if keys:
key = keys[0]
else:
try:
key = Key[parts[-1].upper()]
except KeyError:
return None
return InputBinding(key=key, ctrl="ctrl" in mods, shift="shift" in mods, alt="alt" in mods)
[docs]
class InputEventKey:
"""Keyboard input event with rich metadata."""
__slots__ = ("key", "pressed", "echo", "shift", "ctrl", "alt", "meta", "handled")
def __init__(
self,
key: Key,
pressed: bool,
echo: bool = False,
shift: bool = False,
ctrl: bool = False,
alt: bool = False,
meta: bool = False,
):
self.key = key
self.pressed = pressed
self.echo = echo
self.shift = shift
self.ctrl = ctrl
self.alt = alt
self.meta = meta
self.handled = False
[docs]
def __repr__(self) -> str:
mods = []
if self.shift:
mods.append("Shift")
if self.ctrl:
mods.append("Ctrl")
if self.alt:
mods.append("Alt")
if self.meta:
mods.append("Meta")
prefix = "+".join(mods) + "+" if mods else ""
action = "pressed" if self.pressed else "released"
return f"InputEventKey({prefix}{self.key.name} {action})"
[docs]
class InputEventMouse:
"""Mouse button input event."""
__slots__ = ("button", "pressed", "position", "shift", "ctrl", "alt", "meta", "handled")
def __init__(
self,
button: MouseButton,
pressed: bool,
position: tuple[float, float] = (0.0, 0.0),
shift: bool = False,
ctrl: bool = False,
alt: bool = False,
meta: bool = False,
):
self.button = button
self.pressed = pressed
self.position = position
self.shift = shift
self.ctrl = ctrl
self.alt = alt
self.meta = meta
self.handled = False
[docs]
def __repr__(self) -> str:
action = "pressed" if self.pressed else "released"
return f"InputEventMouse({self.button.name} {action} at {self.position})"