Source code for simvx.core.input.controls

"""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 DeviceButton(DeviceControl): """A button: a key, a mouse or pen button, a finger's contact, a tablet's express key. A button of the abstract pointer type is a **role**, a group of buttons on the device types that inherit it: ``event.button in Pointer.PRIMARY``. A concrete button contains itself and nothing else, so ``event.button in target`` is one test whichever the target is. """ __slots__ = ()
[docs] def __contains__(self, control: object) -> bool: """Whether *control* is this one, or a member of it where this is a group. A group is a button that stands for several: ``Pointer.PRIMARY`` for a left click, a pen tip and a finger, ``Key.CTRL`` for either Control key. A concrete control contains itself alone. A member on a unit is still a member. """ if control is self: return True members = _GROUP_MEMBERS.get(self) if members is None or not isinstance(control, DeviceButton): return False bare = control.on(None) if control.unit is not None else control if bare in members: return True # A finger gesture joins the secondary press where the tree in play # says so (``Input.touch_secondary``); nowhere by default. return self is Pointer.SECONDARY and bare.device is DeviceType.TOUCH and bare is _touch_secondary()
[docs] @property def is_role(self) -> bool: """Whether this is a role of the abstract pointer rather than one device type's button.""" return self.device is DeviceType.POINTER
[docs] @property def is_group(self) -> bool: """Whether this stands for several controls: a pointer role, or a modifier's two sides.""" return self.on(None) in _GROUP_MEMBERS
[docs] @property def members(self) -> frozenset[DeviceButton]: """The controls this group stands for; a concrete control is its own only member.""" return _GROUP_MEMBERS.get(self.on(None), frozenset({self}))
[docs] def __add__(self, other: DeviceButton | DeviceAxis) -> InputBinding: """A chord: *other* pressed while this is held. ``Key.CTRL + Key.S``, ``Controller.LB + Controller.A``.""" from .events import InputBinding return InputBinding(other).holding(self)
[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 button(cls, index: int) -> DeviceButton: """The button at *index* on this device type, whatever the layout calls it. For a button the layout does not name, such as the twelfth express key of a tablet nobody described: ``Tablet.button(11)``. A binding on it hears that index; a binding on a name hears whichever index the layout puts the name at. """ return DeviceButton(cls.device, index=index)
[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 button(cls, index: int) -> DeviceButton: raise ValueError("a pointer role has no index: bind the device's own control, such as Mouse.button(5)")
[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, ()))