"""Controls: every button and axis of every device, as one shape of value.
An input is a **control of a device type**: ``Key.SPACE``, ``Mouse.LEFT``,
``Pen.BARREL_1``, ``Tablet.RING``, ``Tablet.button(11)``. There is no bare
``A`` or ``LEFT``: a name exists only relative to its device type, and the
namespaces below (:class:`Key`, :class:`Mouse`, :class:`Touch`, :class:`Pen`,
:class:`Tablet`, :class:`Controller`, :class:`Pointer`) hold the named ones and
build the rest.
A control is ``(device type, name)`` or ``(device type, index)``, optionally
pinned to one **unit** with :meth:`DeviceControl.on`. A device reports its
buttons and axes by index and its layout
(:mod:`simvx.core.input.layouts`) names them, so an event carries the named
control where the layout has a name and the index form where it has none.
Controls are **interned**: building one with the same parts twice returns the
same object. So equality is identity, ``event.key is Key.S`` and
``event.button == Mouse.LEFT`` agree, and a control is a cheap dict key and set
member.
``Pointer.PRIMARY`` and ``Pointer.SECONDARY`` are **roles**: controls of the
abstract pointer device type that the mouse, a pen and a finger inherit. A role
is a group, answered with ``in``::
event.button in Pointer.PRIMARY # a left click, a pen tip or a finger
Mouse.LEFT != Pointer.PRIMARY # a control is not the group it belongs to
"""
from __future__ import annotations
from collections.abc import Callable, Iterator, Mapping
from enum import StrEnum
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, ClassVar, Self, cast
if TYPE_CHECKING:
from .events import InputBinding
__all__ = [
"DeviceAxis",
"DeviceButton",
"DeviceControl",
"DeviceControls",
"DeviceStick",
"DeviceType",
"Controller",
"Key",
"Mouse",
"Pen",
"Pointer",
"Tablet",
"Touch",
"key_code",
"key_for_code",
"key_to_name",
"name_to_keys",
"roles_of",
]
[docs]
class DeviceType(StrEnum):
"""What kind of device has a control.
A pointer's type has the same spelling as its
:class:`~simvx.core.input.enums.PointerType`, so one converts to the other
by value. :attr:`POINTER` is abstract: no device is one, and the mouse, a
pen and a finger inherit its controls, the roles.
"""
KEYBOARD = "keyboard"
MOUSE = "mouse"
TOUCH = "touch"
PEN = "pen"
#: A tablet's express keys, rings and strips: buttons and axes with no position.
TABLET = "tablet"
CONTROLLER = "controller"
#: The abstract pointer the mouse, a pen and a finger inherit.
POINTER = "pointer"
#: What each device type's namespace is called, for ``repr``.
_NAMESPACE_NAMES: dict[DeviceType, str] = {
DeviceType.KEYBOARD: "Key",
DeviceType.MOUSE: "Mouse",
DeviceType.TOUCH: "Touch",
DeviceType.PEN: "Pen",
DeviceType.TABLET: "Tablet",
DeviceType.CONTROLLER: "Controller",
DeviceType.POINTER: "Pointer",
}
#: Every control built so far, by its parts. Building one again returns it.
_INTERNED: dict[tuple, DeviceControl] = {}
#: The named controls of each device type, by name: what the written spelling
#: and a layout file are checked against.
_NAMED: dict[DeviceType, dict[str, DeviceControl]] = {}
def _check_name(name: str) -> str:
if not name or not name.replace("_", "").isalnum() or name != name.lower():
raise ValueError(
f"control name {name!r} must be lower-case letters, digits and underscores, such as 'barrel_1'"
)
return name
def _unit_of(device: object) -> int | None:
"""The engine id *device* names: an ``InputDevice``, an id, or None for no unit."""
if device is None:
return None
unit = getattr(device, "id", device)
if not isinstance(unit, int) or isinstance(unit, bool):
raise TypeError(f"a unit is a device or its id, got {device!r}")
return unit
[docs]
class DeviceControl:
"""One control of one device type: a button, an axis or a stick.
Exactly one of :attr:`name` and :attr:`index` is set. :attr:`unit`, when
set, pins the control to one device, the engine's id for it.
Built through the namespaces rather than by hand: ``Mouse.LEFT``,
``Tablet.button(11)``, ``Controller.A.on(pad)``. Interned, immutable and
hashable.
"""
__slots__ = ("device", "index", "name", "unit")
device: DeviceType
name: str | None
index: int | None
unit: int | None
[docs]
def __new__(
cls, device: DeviceType | str, name: str | None = None, *, index: int | None = None, unit: int | None = None
) -> Self:
return cls._build(DeviceType(device), name, index, unit)
@classmethod
def _build(cls, device: DeviceType, name: str | None, index: int | None, unit: int | None, *extra: Any) -> Self:
key = (cls, device, name, index, unit, *extra)
found = _INTERNED.get(key)
if found is not None:
return found # type: ignore[return-value]
cls._validate(device, name, index, unit, *extra)
control = object.__new__(cls)
object.__setattr__(control, "device", device)
object.__setattr__(control, "name", name)
object.__setattr__(control, "index", index)
object.__setattr__(control, "unit", unit)
control._set_extra(*extra)
return _INTERNED.setdefault(key, control) # type: ignore[return-value]
@classmethod
def _validate(cls, device: DeviceType, name: str | None, index: int | None, unit: int | None, *extra: Any) -> None:
what = cls.__name__
if (name is None) == (index is None):
raise TypeError(f"{what} names one control: give a name or index=, not {'both' if name else 'neither'}")
if name is not None:
_check_name(name)
elif index is not None and (not isinstance(index, int) or isinstance(index, bool) or index < 0):
raise ValueError(f"{what} index must be an int of 0 or more, got {index!r}")
if device is DeviceType.POINTER:
if index is not None:
raise ValueError("a pointer role has no index: the pointer is abstract, and a role names its members")
if unit is not None:
raise ValueError("a pointer role names no unit: pin the device's own control, such as Pen.TIP.on(pen)")
def _set_extra(self, *extra: Any) -> None:
pass
def _parts(self) -> tuple:
return (self.device, self.name, self.index, self.unit)
[docs]
def __setattr__(self, name: str, value: object) -> None:
raise AttributeError(f"{type(self).__name__} is immutable")
[docs]
def __delattr__(self, name: str) -> None:
raise AttributeError(f"{type(self).__name__} is immutable")
[docs]
def __reduce__(self) -> tuple:
return (_rebuild, (type(self), *self._parts()))
[docs]
def __copy__(self) -> Self:
return self
[docs]
def __deepcopy__(self, memo: dict) -> Self:
return self
# -- Forms ---------------------------------------------------------------
[docs]
def on(self, device: object) -> Self:
"""This control on one unit only: an ``InputDevice``, or its id.
``Controller.A.on(pad)`` is player two's A in local co-op; a binding on it
hears that pad and no other. ``on(None)`` is the control on any unit.
"""
kind = getattr(device, "type", None)
if kind is not None and kind is not self.device:
raise ValueError(f"{self!r} is a {self.device.value} control, and {device!r} is a {kind.value}")
return self._with(unit=_unit_of(device))
def _with(self, **changes: Any) -> Self:
parts = dict(zip(("device", "name", "index", "unit"), self._parts()[:4], strict=True))
parts.update(changes)
return type(self)._build(parts["device"], parts["name"], parts["index"], parts["unit"])
[docs]
@property
def is_named(self) -> bool:
"""Whether a layout name, rather than an index, identifies this control."""
return self.name is not None
# -- Spelling ------------------------------------------------------------
def _part(self) -> str:
return self.name if self.name is not None else f"{self._index_word}({self.index})"
_index_word: ClassVar[str] = "button"
[docs]
def __str__(self) -> str:
"""The written spelling a rebind file stores: ``"mouse:left"``, ``"tablet:button(11)"``.
The unit is not part of it: a file keys a unit by the device's guid or
serial, never by the engine's id, which means nothing in another run.
"""
return f"{self.device.value}:{self._part()}"
[docs]
def __repr__(self) -> str:
space = _NAMESPACE_NAMES[self.device]
if self.name is not None:
named = _NAMED.get(self.device, {}).get(self.name)
text = (
f"{space}.{self.name.upper()}"
if named is not None and type(named) is type(self)
else f"{type(self).__name__}(DeviceType.{self.device.name}, {self.name!r})"
)
else:
text = f"{space}.{self._index_word}({self.index})"
return text + (f".on({self.unit})" if self.unit is not None else "")
[docs]
@staticmethod
def parse(spelling: str) -> DeviceControl:
"""The control a written spelling names, as :meth:`__str__` writes it.
``"mouse:left"``, ``"tablet:button(11)"``, ``"controller:left_x.positive"``,
``"tablet:ring.delta"``, ``"pointer:primary"``. A name the device type
does not have raises, naming the ones it does, so a misspelt control in
a file is an error rather than a binding that never fires.
"""
device_part, sep, rest = spelling.partition(":")
if not sep:
raise ValueError(f"control spelling {spelling!r} names no device type; write it as 'mouse:left'")
try:
device = DeviceType(device_part)
except ValueError:
types = ", ".join(repr(t.value) for t in DeviceType)
raise ValueError(
f"control spelling {spelling!r} names no device type {device_part!r}; types are {types}"
) from None
form = ""
for suffix in (".positive", ".negative", ".delta"):
if rest.endswith(suffix):
rest, form = rest[: -len(suffix)], suffix[1:]
break
control: DeviceControl
for word, cls in (("button", DeviceButton), ("axis", DeviceAxis)):
if rest.startswith(f"{word}(") and rest.endswith(")"):
try:
index = int(rest[len(word) + 1 : -1])
except ValueError:
raise ValueError(f"control spelling {spelling!r}: an index is written {word}(N)") from None
control = cls(device, index=index)
break
else:
named = _NAMED.get(device, {})
found = named.get(rest)
if found is None:
known = ", ".join(sorted(named)) or "none"
raise ValueError(f"{device.value} has no control called {rest!r}; its controls are {known}")
control = found
if form:
if not isinstance(control, DeviceAxis):
raise ValueError(f"control spelling {spelling!r}: .{form} applies to an axis, and {rest!r} is not one")
control = getattr(control, form)
return control
def _as_source(self) -> Any:
"""This control as an action reads it: a whole axis or a stick over its halves, else a binding."""
from .actions import to_source
return to_source(cast(Any, self))
[docs]
def __mul__(self, factor: float) -> Any:
"""This control's value multiplied into an action's unit: ``Mouse.WHEEL * 0.1``.
A whole axis or a stick keeps its sign and direction, so the product
is the composite an action reads it as, each part scaled.
"""
if not isinstance(factor, int | float) or isinstance(factor, bool):
return NotImplemented
return self._as_source() * factor
__rmul__ = __mul__
[docs]
def per_second(self, rate: float) -> Any:
"""This control as motion: its held value times *rate* each second.
``Controller.RIGHT_TRIGGER.per_second(1.0)`` held fully reads 1.0 a
second, over the frame or tick it is read in.
"""
return self._as_source().per_second(rate)
def _rebuild(cls: type[DeviceControl], *parts: Any) -> DeviceControl:
return cls._build(*parts)
[docs]
class DeviceAxis(DeviceControl):
"""An axis: a stick's or a trigger's travel, a tablet's ring or strip.
Three forms of one axis are controls of their own, for what a binding
needs to say:
- :attr:`positive` and :attr:`negative`: one half of the travel. An
``Axis2D`` direction binds a half, and so does a button action driven by
a trigger.
- :attr:`delta`: the motion this frame rather than the position, so a ring
bound this way is a scroll wheel and bound plainly is a dial.
"""
__slots__ = ("half", "is_delta")
half: int
is_delta: bool
_index_word: ClassVar[str] = "axis"
[docs]
def __new__(
cls,
device: DeviceType | str,
name: str | None = None,
*,
index: int | None = None,
unit: int | None = None,
half: int = 0,
is_delta: bool = False,
) -> Self:
return cls._build(DeviceType(device), name, index, unit, half, is_delta)
@classmethod
def _validate(cls, device: DeviceType, name: str | None, index: int | None, unit: int | None, *extra: Any) -> None:
super()._validate(device, name, index, unit)
half, is_delta = extra
if device is DeviceType.POINTER or device is DeviceType.KEYBOARD:
raise ValueError(f"a {device.value} has no axes")
if half not in (-1, 0, 1):
raise ValueError(f"an axis half is -1, 0 or 1, got {half!r}")
if not isinstance(is_delta, bool):
raise TypeError(f"is_delta must be a bool, got {is_delta!r}")
def _set_extra(self, *extra: Any) -> None:
half, is_delta = extra
object.__setattr__(self, "half", half)
object.__setattr__(self, "is_delta", is_delta)
def _parts(self) -> tuple:
return (self.device, self.name, self.index, self.unit, self.half, self.is_delta)
def _with(self, **changes: Any) -> Self:
parts = dict(zip(("device", "name", "index", "unit", "half", "is_delta"), self._parts(), strict=True))
parts.update(changes)
return type(self)._build(
parts["device"], parts["name"], parts["index"], parts["unit"], parts["half"], parts["is_delta"]
)
[docs]
@property
def positive(self) -> DeviceAxis:
"""The half of this axis's travel above zero: right on an x axis, down on a y axis."""
return self._with(half=1)
[docs]
@property
def negative(self) -> DeviceAxis:
"""The half of this axis's travel below zero."""
return self._with(half=-1)
[docs]
@property
def delta(self) -> DeviceAxis:
"""This axis read as its motion this frame rather than its position."""
return self._with(is_delta=True)
[docs]
@property
def whole(self) -> DeviceAxis:
"""This axis itself: both halves, read as its position, on any unit."""
return self._with(half=0, is_delta=False, unit=None)
def _part(self) -> str:
base = super()._part()
suffix = {1: ".positive", -1: ".negative"}.get(self.half, "")
return base + suffix + (".delta" if self.is_delta else "")
[docs]
def __repr__(self) -> str:
base = DeviceControl.__repr__(self._with(half=0, is_delta=False, unit=None))
suffix = {1: ".positive", -1: ".negative"}.get(self.half, "") + (".delta" if self.is_delta else "")
return base + suffix + (f".on({self.unit})" if self.unit is not None else "")
[docs]
class DeviceStick(DeviceControl):
"""Two axes that move together: a controller's stick.
A stick in an action's binding list is the ``Axis2D`` over its four
halves, and its directions are controls of their own for a rebind screen:
``Controller.LEFT_STICK.right`` is ``Controller.LEFT_X.positive``. ``+y`` is down
the screen, as everywhere in 2D.
"""
__slots__ = ()
@classmethod
def _validate(cls, device: DeviceType, name: str | None, index: int | None, unit: int | None, *extra: Any) -> None:
super()._validate(device, name, index, unit)
if name is None:
raise TypeError("a stick is named; bind an unnamed pair of axes as Axis2D")
if (device, name) not in _STICK_AXES:
known = ", ".join(sorted(str(n) for d, n in _STICK_AXES if d is device)) or "none"
raise ValueError(f"{device.value} has no stick called {name!r}; its sticks are {known}")
[docs]
@property
def x(self) -> DeviceAxis:
"""The stick's horizontal axis, on this stick's unit."""
return _STICK_AXES[(self.device, self.name)][0].on(self.unit)
[docs]
@property
def y(self) -> DeviceAxis:
"""The stick's vertical axis, ``+y`` down, on this stick's unit."""
return _STICK_AXES[(self.device, self.name)][1].on(self.unit)
[docs]
@property
def up(self) -> DeviceAxis:
return self.y.negative
[docs]
@property
def down(self) -> DeviceAxis:
return self.y.positive
[docs]
@property
def left(self) -> DeviceAxis:
return self.x.negative
[docs]
@property
def right(self) -> DeviceAxis:
return self.x.positive
#: Which two axes each stick is, by device type and stick name.
_STICK_AXES: dict[tuple[DeviceType, str | None], tuple[DeviceAxis, DeviceAxis]] = {}
# ---------------------------------------------------------------------------
# The namespaces
# ---------------------------------------------------------------------------
class _Namespace(type):
"""The metaclass that makes a device type's namespace iterable and testable."""
_controls: tuple[DeviceControl, ...]
device: DeviceType
def __iter__(cls) -> Iterator[DeviceControl]:
"""Every named control of this device type, in declaration order."""
return iter(cls._controls)
def __len__(cls) -> int:
return len(cls._controls)
def __contains__(cls, control: object) -> bool:
"""Whether *control* is one of this device type's, named or not, on any unit."""
return isinstance(control, DeviceControl) and control.device is cls.device
def __repr__(cls) -> str:
return cls.__name__
[docs]
class DeviceControls(metaclass=_Namespace):
"""A device type's controls: the named ones as attributes, the rest built by index.
A namespace, not a type: ``Mouse.LEFT`` is a :class:`DeviceButton`, and
nothing is ever an instance of ``Mouse``. Iterating one lists its named
controls, which is what a rebind screen offers; ``x in Pen`` asks whether
*x* is a pen's control.
"""
device: ClassVar[DeviceType]
_controls: ClassVar[tuple[DeviceControl, ...]] = ()
[docs]
def __init_subclass__(cls, *, device: DeviceType, **kwargs: Any) -> None:
super().__init_subclass__(**kwargs)
cls.device = device
found: list[DeviceControl] = []
named = _NAMED.setdefault(device, {})
for attr, value in vars(cls).items():
if not isinstance(value, DeviceControl) or attr.startswith("_"):
continue
if value.device is not device or value.name is None or value.name.upper() != attr:
raise TypeError(f"{cls.__name__}.{attr} must be the {device.value} control named {attr.lower()!r}")
if value.name in named and named[value.name] is not value:
raise TypeError(f"{device.value} names {value.name!r} twice")
named[value.name] = value
found.append(value)
cls._controls = tuple(found)
[docs]
def __new__(cls, *args: object, **kwargs: object) -> DeviceControls:
raise TypeError(f"{cls.__name__} is a namespace of controls, not a type: use {cls.__name__}.<NAME>")
[docs]
@classmethod
def axis(cls, index: int) -> DeviceAxis:
"""The axis at *index* on this device type, whatever the layout calls it."""
return DeviceAxis(cls.device, index=index)
def _key(name: str, code: int) -> DeviceButton:
"""A named key, recorded with the physical position it is on the engine's key table."""
key = DeviceButton(DeviceType.KEYBOARD, name)
_KEY_CODES[key] = code
_KEYS_BY_CODE[code] = key
return key
#: The engine's key table: each named key's physical position, as GLFW numbers it.
_KEY_CODES: dict[DeviceButton, int] = {}
_KEYS_BY_CODE: dict[int, DeviceButton] = {}
[docs]
class Key(DeviceControls, device=DeviceType.KEYBOARD):
"""The keyboard's keys, by **physical position**.
``Key.W`` is the key where W sits on a US board, so WASD is the same four
positions on QWERTY and on AZERTY. The keyboard's layout is fixed, which is
why its keys cannot be overridden the way a pen's barrels can: a key's
position already is the hardware fact, and moving what it does is a rebind.
A platform code the table does not name is ``Key.button(code)``.
"""
# Printable keys
SPACE = _key("space", 32)
APOSTROPHE = _key("apostrophe", 39) # '
COMMA = _key("comma", 44) # ,
MINUS = _key("minus", 45) # -
PERIOD = _key("period", 46) # .
SLASH = _key("slash", 47) # /
# Digits
KEY_0 = _key("key_0", 48)
KEY_1 = _key("key_1", 49)
KEY_2 = _key("key_2", 50)
KEY_3 = _key("key_3", 51)
KEY_4 = _key("key_4", 52)
KEY_5 = _key("key_5", 53)
KEY_6 = _key("key_6", 54)
KEY_7 = _key("key_7", 55)
KEY_8 = _key("key_8", 56)
KEY_9 = _key("key_9", 57)
SEMICOLON = _key("semicolon", 59) # ;
EQUAL = _key("equal", 61) # =
# Letters
A = _key("a", 65)
B = _key("b", 66)
C = _key("c", 67)
D = _key("d", 68)
E = _key("e", 69)
F = _key("f", 70)
G = _key("g", 71)
H = _key("h", 72)
I = _key("i", 73) # noqa: E741
J = _key("j", 74)
K = _key("k", 75)
L = _key("l", 76)
M = _key("m", 77)
N = _key("n", 78)
O = _key("o", 79) # noqa: E741
P = _key("p", 80)
Q = _key("q", 81)
R = _key("r", 82)
S = _key("s", 83)
T = _key("t", 84)
U = _key("u", 85)
V = _key("v", 86)
W = _key("w", 87)
X = _key("x", 88)
Y = _key("y", 89)
Z = _key("z", 90)
LEFT_BRACKET = _key("left_bracket", 91) # [
BACKSLASH = _key("backslash", 92) # backslash
RIGHT_BRACKET = _key("right_bracket", 93) # ]
GRAVE_ACCENT = _key("grave_accent", 96) # `
# Function keys
ESCAPE = _key("escape", 256)
ENTER = _key("enter", 257)
TAB = _key("tab", 258)
BACKSPACE = _key("backspace", 259)
INSERT = _key("insert", 260)
DELETE = _key("delete", 261)
RIGHT = _key("right", 262)
LEFT = _key("left", 263)
DOWN = _key("down", 264)
UP = _key("up", 265)
PAGE_UP = _key("page_up", 266)
PAGE_DOWN = _key("page_down", 267)
HOME = _key("home", 268)
END = _key("end", 269)
CAPS_LOCK = _key("caps_lock", 280)
SCROLL_LOCK = _key("scroll_lock", 281)
NUM_LOCK = _key("num_lock", 282)
PRINT_SCREEN = _key("print_screen", 283)
PAUSE = _key("pause", 284)
F1 = _key("f1", 290)
F2 = _key("f2", 291)
F3 = _key("f3", 292)
F4 = _key("f4", 293)
F5 = _key("f5", 294)
F6 = _key("f6", 295)
F7 = _key("f7", 296)
F8 = _key("f8", 297)
F9 = _key("f9", 298)
F10 = _key("f10", 299)
F11 = _key("f11", 300)
F12 = _key("f12", 301)
# Keypad
KP_0 = _key("kp_0", 320)
KP_1 = _key("kp_1", 321)
KP_2 = _key("kp_2", 322)
KP_3 = _key("kp_3", 323)
KP_4 = _key("kp_4", 324)
KP_5 = _key("kp_5", 325)
KP_6 = _key("kp_6", 326)
KP_7 = _key("kp_7", 327)
KP_8 = _key("kp_8", 328)
KP_9 = _key("kp_9", 329)
KP_DECIMAL = _key("kp_decimal", 330)
KP_DIVIDE = _key("kp_divide", 331)
KP_MULTIPLY = _key("kp_multiply", 332)
KP_SUBTRACT = _key("kp_subtract", 333)
KP_ADD = _key("kp_add", 334)
KP_ENTER = _key("kp_enter", 335)
# Modifiers
LEFT_SHIFT = _key("left_shift", 340)
LEFT_CONTROL = _key("left_control", 341)
LEFT_ALT = _key("left_alt", 342)
LEFT_SUPER = _key("left_super", 343)
RIGHT_SHIFT = _key("right_shift", 344)
RIGHT_CONTROL = _key("right_control", 345)
RIGHT_ALT = _key("right_alt", 346)
RIGHT_SUPER = _key("right_super", 347)
MENU = _key("menu", 348)
# Either side of a modifier: a group, bound or held as one key. Bind a side
# on its own with LEFT_CONTROL, RIGHT_SHIFT and the rest.
CTRL = DeviceButton(DeviceType.KEYBOARD, "ctrl")
SHIFT = DeviceButton(DeviceType.KEYBOARD, "shift")
ALT = DeviceButton(DeviceType.KEYBOARD, "alt")
META = DeviceButton(DeviceType.KEYBOARD, "meta")
[docs]
@classmethod
def axis(cls, index: int) -> DeviceAxis:
raise ValueError("a keyboard has no axes")
_M = DeviceType.MOUSE
_T = DeviceType.TOUCH
_P = DeviceType.PEN
_TB = DeviceType.TABLET
[docs]
class Mouse(DeviceControls, device=_M):
"""The mouse's buttons and its wheel. A pen's barrel is never one: bind ``Pointer.SECONDARY`` for both."""
LEFT = DeviceButton(_M, "left")
RIGHT = DeviceButton(_M, "right")
MIDDLE = DeviceButton(_M, "middle")
BACK = DeviceButton(_M, "back")
FORWARD = DeviceButton(_M, "forward")
#: The wheel, as motion: a binding reads the frame's scroll, positive
#: rolling up, and ``Mouse.WHEEL.positive`` is a notch up as a button. A
#: scroll a control took (a list scrolling) moves no binding.
WHEEL = DeviceAxis(_M, "wheel")
[docs]
class Touch(DeviceControls, device=_T):
"""A finger's contact with the glass, and the gestures fingers make together.
A gesture is recognised only while a binding or a handler names it, so a
game that names none pays nothing for them. Each axis is motion: a
binding reads the frame's change and rests at zero in a frame without one.
"""
#: A finger touching the glass.
CONTACT = DeviceButton(_T, "contact")
#: A finger held still: pressed once it has rested long enough
#: (``tree.long_press_time``) without travelling, released as it lifts.
LONG_PRESS = DeviceButton(_T, "long_press")
#: Two fingers tapped together: pressed and released as they lift.
TWO_FINGER_TAP = DeviceButton(_T, "two_finger_tap")
#: Two fingers spreading or closing: the frame's change of scale, as a
#: fraction (0.05 is five percent wider), positive spreading.
PINCH = DeviceAxis(_T, "pinch")
#: Two fingers turning: the frame's change of angle, radians, clockwise.
ROTATE = DeviceAxis(_T, "rotate")
#: Two fingers moving together: the frame's translation, logical pixels.
PAN_X = DeviceAxis(_T, "pan_x")
PAN_Y = DeviceAxis(_T, "pan_y")
_STICK_AXES[(_T, "pan")] = (PAN_X, PAN_Y)
#: ``PAN_X`` and ``PAN_Y`` together, ``+y`` down.
PAN = DeviceStick(_T, "pan")
[docs]
class Pen(DeviceControls, device=_P):
"""A pen's buttons. The eraser is not here: it is a tool (``PointerTool.ERASER``), not a button."""
TIP = DeviceButton(_P, "tip")
BARREL_1 = DeviceButton(_P, "barrel_1")
BARREL_2 = DeviceButton(_P, "barrel_2")
BARREL_3 = DeviceButton(_P, "barrel_3")
[docs]
class Tablet(DeviceControls, device=_TB):
"""A tablet's own controls: express keys by index, and the rings and strips a layout names.
The pen on the tablet is :class:`Pen`. No express key has a name the
engine could promise, so they are ``Tablet.button(i)``; a backend that
knows the device names its rings and strips.
"""
RING = DeviceAxis(_TB, "ring")
RING_2 = DeviceAxis(_TB, "ring_2")
STRIP = DeviceAxis(_TB, "strip")
STRIP_2 = DeviceAxis(_TB, "strip_2")
_G = DeviceType.CONTROLLER
[docs]
class Controller(DeviceControls, device=_G):
"""A pad's controls, by position: ``Controller.A`` is the bottom face button on every pad.
What is printed on that button moves with the family (Cross on a
PlayStation pad, B on a Nintendo one), and ``ControllerDevice.label`` says
what; the control does not. A pad no mapping database knows reports the
driver's own indices, which bind as ``Controller.button(i)`` and
``Controller.axis(i)`` until a layout names them.
Sticks run -1 to 1 per axis, ``+y`` down; triggers run 0 released to 1
pulled.
"""
A = DeviceButton(_G, "a")
B = DeviceButton(_G, "b")
X = DeviceButton(_G, "x")
Y = DeviceButton(_G, "y")
LEFT_BUMPER = DeviceButton(_G, "left_bumper")
RIGHT_BUMPER = DeviceButton(_G, "right_bumper")
BACK = DeviceButton(_G, "back")
START = DeviceButton(_G, "start")
GUIDE = DeviceButton(_G, "guide")
LEFT_THUMB = DeviceButton(_G, "left_thumb")
RIGHT_THUMB = DeviceButton(_G, "right_thumb")
DPAD_UP = DeviceButton(_G, "dpad_up")
DPAD_RIGHT = DeviceButton(_G, "dpad_right")
DPAD_DOWN = DeviceButton(_G, "dpad_down")
DPAD_LEFT = DeviceButton(_G, "dpad_left")
LEFT_X = DeviceAxis(_G, "left_x")
LEFT_Y = DeviceAxis(_G, "left_y")
RIGHT_X = DeviceAxis(_G, "right_x")
RIGHT_Y = DeviceAxis(_G, "right_y")
LEFT_TRIGGER = DeviceAxis(_G, "left_trigger")
RIGHT_TRIGGER = DeviceAxis(_G, "right_trigger")
_STICK_AXES[(_G, "left_stick")] = (LEFT_X, LEFT_Y)
_STICK_AXES[(_G, "right_stick")] = (RIGHT_X, RIGHT_Y)
LEFT_STICK = DeviceStick(_G, "left_stick")
RIGHT_STICK = DeviceStick(_G, "right_stick")
[docs]
class Pointer(DeviceControls, device=DeviceType.POINTER):
"""The roles every pointer inherits: its main press and its alternate press.
Kept to the two whose meaning genuinely crosses devices. Back, forward and
the middle click are the mouse's own, because nothing else means them.
"""
#: The main press: the mouse's left, a pen's tip, a finger's contact.
PRIMARY = DeviceButton(DeviceType.POINTER, "primary")
#: The alternate press: the mouse's right, a pen's first barrel button.
SECONDARY = DeviceButton(DeviceType.POINTER, "secondary")
[docs]
@classmethod
def axis(cls, index: int) -> DeviceAxis:
raise ValueError("the abstract pointer has no axes")
def _no_touch_secondary() -> DeviceButton | None:
return None
#: Answers which finger gesture is a secondary press in the tree in play;
#: the input state installs the real one, which reads ``Input.touch_secondary``.
_touch_secondary: Callable[[], DeviceButton | None] = _no_touch_secondary
#: What each role groups, by name, so a layout that moves a name moves its role.
_ROLE_MEMBERS: dict[DeviceButton, frozenset[DeviceButton]] = {
Pointer.PRIMARY: frozenset({Mouse.LEFT, Pen.TIP, Touch.CONTACT}),
Pointer.SECONDARY: frozenset({Mouse.RIGHT, Pen.BARREL_1}),
}
#: Each modifier's two sides: ``Key.CTRL`` is either Control key, and so on.
MODIFIER_SIDES: Mapping[DeviceButton, frozenset[DeviceButton]] = MappingProxyType(
{
Key.CTRL: frozenset({Key.LEFT_CONTROL, Key.RIGHT_CONTROL}),
Key.SHIFT: frozenset({Key.LEFT_SHIFT, Key.RIGHT_SHIFT}),
Key.ALT: frozenset({Key.LEFT_ALT, Key.RIGHT_ALT}),
Key.META: frozenset({Key.LEFT_SUPER, Key.RIGHT_SUPER}),
}
)
#: The modifier each side key belongs to: ``Key.CTRL`` for ``Key.RIGHT_CONTROL``.
MODIFIER_OF: Mapping[DeviceButton, DeviceButton] = MappingProxyType(
{side: group for group, sides in MODIFIER_SIDES.items() for side in sides}
)
#: Every group and what it stands for: the pointer roles and the modifiers.
_GROUP_MEMBERS: dict[DeviceButton, frozenset[DeviceButton]] = {**_ROLE_MEMBERS, **MODIFIER_SIDES}
#: The roles each control belongs to, the reverse of the table above.
_ROLES_OF: dict[DeviceButton, tuple[DeviceButton, ...]] = {}
for _role, _members in _ROLE_MEMBERS.items():
for _member in _members:
_ROLES_OF[_member] = (*_ROLES_OF.get(_member, ()), _role)
#: Each role's member on each device type: what a layout resolves a role to.
ROLE_MEMBER: Mapping[tuple[DeviceButton, DeviceType], DeviceButton] = MappingProxyType(
{(role, member.device): member for role, members in _ROLE_MEMBERS.items() for member in members}
)
[docs]
def roles_of(control: DeviceControl) -> tuple[DeviceButton, ...]:
"""The roles *control* belongs to: ``(Pointer.PRIMARY,)`` for ``Mouse.LEFT``, ``()`` for most."""
if control.unit is not None:
control = control.on(None)
if not isinstance(control, DeviceButton):
return ()
return _ROLES_OF.get(control, ())
# ---------------------------------------------------------------------------
# Keys and their codes
# ---------------------------------------------------------------------------
[docs]
def key_code(key: DeviceControl) -> int | None:
"""The physical position *key* is on the engine's key table, or None for a control that is not a key."""
if key.device is not DeviceType.KEYBOARD or not isinstance(key, DeviceButton):
return None
if key.index is not None:
return key.index
return _KEY_CODES.get(key.on(None) if key.unit is not None else key)
[docs]
def key_for_code(code: int) -> DeviceButton:
"""The key at platform code *code*: the named one where the table has it, else ``Key.button(code)``."""
found = _KEYS_BY_CODE.get(code)
return found if found is not None else DeviceButton(DeviceType.KEYBOARD, index=code)
_KEY_TO_NAME: dict[int, str] = {
32: "space",
39: "'",
44: ",",
45: "-",
46: ".",
47: "/",
59: ";",
61: "=",
91: "[",
92: "\\",
93: "]",
96: "`",
256: "escape",
257: "enter",
258: "tab",
259: "backspace",
260: "insert",
261: "delete",
262: "right",
263: "left",
264: "down",
265: "up",
266: "pageup",
267: "pagedown",
268: "home",
269: "end",
340: "shift",
341: "ctrl",
342: "alt",
343: "super",
344: "shift",
345: "ctrl",
346: "alt",
347: "super",
}
for _code in range(48, 58):
_KEY_TO_NAME[_code] = chr(_code) # '0'..'9'
for _code in range(65, 91):
_KEY_TO_NAME[_code] = chr(_code + 32) # 'a'..'z'
for _i in range(1, 13):
_KEY_TO_NAME[289 + _i] = f"f{_i}"
#: A combo name to every key it can mean: "shift" is either shift.
_NAME_TO_KEYS: dict[str, list[DeviceButton]] = {}
for _code, _name in _KEY_TO_NAME.items():
_NAME_TO_KEYS.setdefault(_name, []).append(_KEYS_BY_CODE[_code])
#: Which modifier flag a modifier key IS, as an index into the ``(ctrl, shift,
#: alt, meta)`` tuple every layer spells modifiers with. A modifier key's own
#: event reports itself from the event rather than from held state, and a filter
#: on that same flag is exempt: "shift must not be held" cannot be satisfied by a
#: Shift press, so testing it would make a modifier-bound handler match no edge.
SELF_MODIFIER: dict[int, int] = {341: 0, 345: 0, 340: 1, 344: 1, 342: 2, 346: 2, 343: 3, 347: 3}
def _name_for_code(code: int) -> str:
"""The combo name for a raw platform key code, whether or not a key exists for it."""
return _KEY_TO_NAME.get(code, f"key_{code}")
[docs]
def key_to_name(key: DeviceButton) -> str:
"""The combo spelling of one key: ``"space"``, ``"shift"``, ``"a"``."""
code = key_code(key)
if code is None:
raise ValueError(f"{key!r} is not a key")
return _name_for_code(code)
[docs]
def name_to_keys(name: str) -> list[DeviceButton]:
"""Every key a combo name can mean: ``"shift"`` is both shifts."""
return list(_NAME_TO_KEYS.get(name, ()))