"""
This file contains InputHandler, an observer-pattern-based dispatcher
mapping keyboard (including modifier combos), mouse (clicks, wheel,
motion), and gamepad (buttons, axes, hotplug) input to named actions,
notifying every registered InputListener when one fires — plus every
KEY_*/MOUSE_*/GAMEPAD_* constant and *Data class involved.
Author: Alejandro Mujica (aledrums@gmail.com)
"""
from typing import Tuple, Union, TypeVar, NoReturn, Dict, List, Optional, Type
import pygame
INPUT_EVENTS: Tuple[int, ...] = (
pygame.KEYDOWN,
pygame.KEYUP,
pygame.MOUSEBUTTONDOWN,
pygame.MOUSEBUTTONUP,
pygame.MOUSEWHEEL,
pygame.MOUSEMOTION,
pygame.CONTROLLERBUTTONDOWN,
pygame.CONTROLLERBUTTONUP,
pygame.CONTROLLERAXISMOTION,
pygame.CONTROLLERDEVICEADDED,
pygame.CONTROLLERDEVICEREMOVED,
)
# Modifier keys that can be combined with a regular key to build a key
# combo, for instance, MOD_CTRL | MOD_SHIFT to represent Ctrl + Shift.
MOD_NONE: int = pygame.KMOD_NONE
MOD_SHIFT: int = pygame.KMOD_SHIFT
MOD_CTRL: int = pygame.KMOD_CTRL
MOD_ALT: int = pygame.KMOD_ALT
MOD_META: int = pygame.KMOD_META
# Modifiers that gale takes into account to resolve key combos. Lock keys
# such as Caps Lock or Num Lock are intentionally ignored.
_COMBINABLE_MODIFIERS: Tuple[int, int, int, int] = (
MOD_SHIFT,
MOD_CTRL,
MOD_ALT,
MOD_META,
)
def _normalize_modifiers(raw_modifiers: int) -> int:
"""
Each MOD_* constant (e.g. MOD_CTRL) is actually the combination of
its left and right variants (e.g. KMOD_LCTRL | KMOD_RCTRL), but a
real key press only ever reports whichever single side was held,
never that combined value. This maps a raw pygame modifier state
to the combination of MOD_* constants a binding could have been
registered with, so holding either (or both) side of a modifier
matches a binding requiring it.
"""
normalized = 0
for family in _COMBINABLE_MODIFIERS:
if raw_modifiers & family:
normalized |= family
return normalized
[docs]
class InvalidListenerException(Exception):
pass
[docs]
class KeyboardData:
"""
Group the data associated to a keyboad input event.
"""
def __init__(self, event: pygame.event.Event) -> None:
self.pressed: bool = event.type == pygame.KEYDOWN
self.released: bool = event.type == pygame.KEYUP
self.modifier: int = event.mod
self.unicode: str = event.unicode
self.key: int = event.key
[docs]
@staticmethod
def get_action_key(event: pygame.event.Event) -> Tuple[int, int]:
return (_normalize_modifiers(event.mod), event.key)
[docs]
@staticmethod
def get_action_name():
return "keyboard"
[docs]
class MouseClickData:
"""
Group the data associated to a mouse click event.
"""
def __init__(self, event: pygame.event.Event) -> None:
self.pressed: bool = event.type == pygame.MOUSEBUTTONDOWN
self.released: bool = event.type == pygame.MOUSEBUTTONUP
self.button: int = event.button
self.position: Tuple[int, int] = event.pos
[docs]
@staticmethod
def get_action_key(event: pygame.event.Event) -> int:
return event.button
[docs]
@staticmethod
def get_action_name():
return "mouse_click"
[docs]
class MouseWheelData:
"""
Group the data associated to a mouse wheel event.
"""
def __init__(self, event: pygame.event.Event) -> None:
self.flipped: bool = event.flipped
[docs]
@staticmethod
def get_action_key(event: pygame.event.Event) -> Tuple[int, int]:
return event.x, event.y
[docs]
@staticmethod
def get_action_name():
return "mouse_wheel"
[docs]
class MouseMotionData:
"""
Group the data associated to a mouse motion event.
"""
def __init__(self, event: pygame.event.Event) -> None:
self.position: Tuple[int, int] = event.pos
self.buttons: Tuple[int, int, int] = event.buttons
[docs]
@staticmethod
def get_action_key(event: pygame.event.Event) -> Tuple[int, int]:
return event.rel
[docs]
@staticmethod
def get_action_name():
return "mouse_motion"
[docs]
class GamepadAxisData:
"""
Group the data associated to a gamepad axis motion event (an
analog stick or a trigger). Fired continuously as the axis moves,
the same way MouseMotionData is: bind it once with
set_gamepad_axis_action and read value every time it is notified,
rather than expecting a single discrete event.
"""
def __init__(self, event: pygame.event.Event) -> None:
value = event.value
# Most pygame builds already normalize this to [-1.0, 1.0]
# ([0.0, 1.0] for a trigger), but fall back to normalizing the
# raw SDL int16 range ([-32768, 32767]) in case a given build
# doesn't, so games never have to special-case this themselves.
self.value: float = value if abs(value) <= 1.0 else value / 32768.0
self.axis: int = event.axis
self.gamepad_id: int = event.instance_id
[docs]
@staticmethod
def get_action_key(event: pygame.event.Event) -> Tuple[int, int]:
return (event.instance_id, event.axis)
[docs]
@staticmethod
def get_action_name():
return "gamepad_axis"
InputData = TypeVar(
"InputData",
bound=Union[
KeyboardData,
MouseClickData,
MouseWheelData,
MouseMotionData,
GamepadButtonData,
GamepadAxisData,
],
)
KEY_0: int = pygame.K_0
KEY_1: int = pygame.K_1
KEY_2: int = pygame.K_2
KEY_3: int = pygame.K_3
KEY_4: int = pygame.K_4
KEY_5: int = pygame.K_5
KEY_6: int = pygame.K_6
KEY_7: int = pygame.K_7
KEY_8: int = pygame.K_8
KEY_9: int = pygame.K_9
KEY_AC_BACK: int = pygame.K_AC_BACK
KEY_AMPERSAND: int = pygame.K_AMPERSAND
KEY_ASTERISK: int = pygame.K_ASTERISK
KEY_AT: int = pygame.K_AT
KEY_BACKQUOTE: int = pygame.K_BACKQUOTE
KEY_BACKSLASH: int = pygame.K_BACKSLASH
KEY_BACKSPACE: int = pygame.K_BACKSPACE
KEY_BREAK: int = pygame.K_BREAK
KEY_CAPSLOCK: int = pygame.K_CAPSLOCK
KEY_CARET: int = pygame.K_CARET
KEY_CLEAR: int = pygame.K_CLEAR
KEY_COLON: int = pygame.K_COLON
KEY_COMMA: int = pygame.K_COMMA
KEY_CURRENCYSUBUNIT: int = pygame.K_CURRENCYSUBUNIT
KEY_CURRENCYUNIT: int = pygame.K_CURRENCYUNIT
KEY_DELETE: int = pygame.K_DELETE
KEY_DOLLAR: int = pygame.K_DOLLAR
KEY_DOWN: int = pygame.K_DOWN
KEY_END: int = pygame.K_END
KEY_EQUALS: int = pygame.K_EQUALS
KEY_ESCAPE: int = pygame.K_ESCAPE
KEY_EURO: int = pygame.K_EURO
KEY_EXCLAIM: int = pygame.K_EXCLAIM
KEY_F1: int = pygame.K_F1
KEY_F10: int = pygame.K_F10
KEY_F11: int = pygame.K_F11
KEY_F12: int = pygame.K_F12
KEY_F13: int = pygame.K_F13
KEY_F14: int = pygame.K_F14
KEY_F15: int = pygame.K_F15
KEY_F2: int = pygame.K_F2
KEY_F3: int = pygame.K_F3
KEY_F4: int = pygame.K_F4
KEY_F5: int = pygame.K_F5
KEY_F6: int = pygame.K_F6
KEY_F7: int = pygame.K_F7
KEY_F8: int = pygame.K_F8
KEY_F9: int = pygame.K_F9
KEY_GREATER: int = pygame.K_GREATER
KEY_HASH: int = pygame.K_HASH
KEY_HELP: int = pygame.K_HELP
KEY_HOME: int = pygame.K_HOME
KEY_INSERT: int = pygame.K_INSERT
KEY_KP0: int = pygame.K_KP0
KEY_KP1: int = pygame.K_KP1
KEY_KP2: int = pygame.K_KP2
KEY_KP3: int = pygame.K_KP3
KEY_KP4: int = pygame.K_KP4
KEY_KP5: int = pygame.K_KP5
KEY_KP6: int = pygame.K_KP6
KEY_KP7: int = pygame.K_KP7
KEY_KP8: int = pygame.K_KP8
KEY_KP9: int = pygame.K_KP9
KEY_KP_0: int = pygame.K_KP_0
KEY_KP_1: int = pygame.K_KP_1
KEY_KP_2: int = pygame.K_KP_2
KEY_KP_3: int = pygame.K_KP_3
KEY_KP_4: int = pygame.K_KP_4
KEY_KP_5: int = pygame.K_KP_5
KEY_KP_6: int = pygame.K_KP_6
KEY_KP_7: int = pygame.K_KP_7
KEY_KP_8: int = pygame.K_KP_8
KEY_KP_9: int = pygame.K_KP_9
KEY_KP_DIVIDE: int = pygame.K_KP_DIVIDE
KEY_KP_ENTER: int = pygame.K_KP_ENTER
KEY_KP_EQUALS: int = pygame.K_KP_EQUALS
KEY_KP_MINUS: int = pygame.K_KP_MINUS
KEY_KP_MULTIPLY: int = pygame.K_KP_MULTIPLY
KEY_KP_PERIOD: int = pygame.K_KP_PERIOD
KEY_KP_PLUS: int = pygame.K_KP_PLUS
KEY_LALT: int = pygame.K_LALT
KEY_LCTRL: int = pygame.K_LCTRL
KEY_LEFT: int = pygame.K_LEFT
KEY_LEFTBRACKET: int = pygame.K_LEFTBRACKET
KEY_LEFTPAREN: int = pygame.K_LEFTPAREN
KEY_LESS: int = pygame.K_LESS
KEY_LGUI: int = pygame.K_LGUI
KEY_LMETA: int = pygame.K_LMETA
KEY_LSHIFT: int = pygame.K_LSHIFT
KEY_LSUPER: int = pygame.K_LSUPER
KEY_MENU: int = pygame.K_MENU
KEY_MINUS: int = pygame.K_MINUS
KEY_MODE: int = pygame.K_MODE
KEY_NUMLOCK: int = pygame.K_NUMLOCK
KEY_NUMLOCKCLEAR: int = pygame.K_NUMLOCKCLEAR
KEY_PAGEDOWN: int = pygame.K_PAGEDOWN
KEY_PAGEUP: int = pygame.K_PAGEUP
KEY_PAUSE: int = pygame.K_PAUSE
KEY_PERCENT: int = pygame.K_PERCENT
KEY_PERIOD: int = pygame.K_PERIOD
KEY_PLUS: int = pygame.K_PLUS
KEY_POWER: int = pygame.K_POWER
KEY_PRINT: int = pygame.K_PRINT
KEY_PRINTSCREEN: int = pygame.K_PRINTSCREEN
KEY_QUESTION: int = pygame.K_QUESTION
KEY_QUOTE: int = pygame.K_QUOTE
KEY_QUOTEDBL: int = pygame.K_QUOTEDBL
KEY_RALT: int = pygame.K_RALT
KEY_RCTRL: int = pygame.K_RCTRL
KEY_RETURN: int = pygame.K_RETURN
KEY_RGUI: int = pygame.K_RGUI
KEY_RIGHT: int = pygame.K_RIGHT
KEY_RIGHTBRACKET: int = pygame.K_RIGHTBRACKET
KEY_RIGHTPAREN: int = pygame.K_RIGHTPAREN
KEY_RMETA: int = pygame.K_RMETA
KEY_RSHIFT: int = pygame.K_RSHIFT
KEY_RSUPER: int = pygame.K_RSUPER
KEY_SCROLLLOCK: int = pygame.K_SCROLLLOCK
KEY_SCROLLOCK: int = pygame.K_SCROLLOCK
KEY_SEMICOLON: int = pygame.K_SEMICOLON
KEY_SLASH: int = pygame.K_SLASH
KEY_SPACE: int = pygame.K_SPACE
KEY_SYSREQ: int = pygame.K_SYSREQ
KEY_TAB: int = pygame.K_TAB
KEY_UNDERSCORE: int = pygame.K_UNDERSCORE
KEY_UNKNOWN: int = pygame.K_UNKNOWN
KEY_UP: int = pygame.K_UP
KEY_a: int = pygame.K_a
KEY_b: int = pygame.K_b
KEY_c: int = pygame.K_c
KEY_d: int = pygame.K_d
KEY_e: int = pygame.K_e
KEY_f: int = pygame.K_f
KEY_g: int = pygame.K_g
KEY_h: int = pygame.K_h
KEY_i: int = pygame.K_i
KEY_j: int = pygame.K_j
KEY_k: int = pygame.K_k
KEY_l: int = pygame.K_l
KEY_m: int = pygame.K_m
KEY_n: int = pygame.K_n
KEY_o: int = pygame.K_o
KEY_p: int = pygame.K_p
KEY_q: int = pygame.K_q
KEY_r: int = pygame.K_r
KEY_s: int = pygame.K_s
KEY_t: int = pygame.K_t
KEY_u: int = pygame.K_u
KEY_v: int = pygame.K_v
KEY_w: int = pygame.K_w
KEY_x: int = pygame.K_x
KEY_y: int = pygame.K_y
KEY_z: int = pygame.K_z
MOUSE_BUTTON_1: int = 1
MOUSE_BUTTON_2: int = 2
MOUSE_BUTTON_3: int = 3
MOUSE_MOTION_UP: Tuple[int, int] = (0, -1)
MOUSE_MOTION_RIGHT: Tuple[int, int] = (1, 0)
MOUSE_MOTION_DOWN: Tuple[int, int] = (0, 1)
MOUSE_MOTION_LEFT: Tuple[int, int] = (-1, 0)
MOUSE_WHEEL_UP: Tuple[int, int] = (0, -1)
MOUSE_WHEEL_RIGHT: Tuple[int, int] = (1, 0)
MOUSE_WHEEL_DOWN: Tuple[int, int] = (0, 1)
MOUSE_WHEEL_LEFT: Tuple[int, int] = (-1, 0)
GAMEPAD_BUTTON_A: int = pygame.CONTROLLER_BUTTON_A
GAMEPAD_BUTTON_B: int = pygame.CONTROLLER_BUTTON_B
GAMEPAD_BUTTON_X: int = pygame.CONTROLLER_BUTTON_X
GAMEPAD_BUTTON_Y: int = pygame.CONTROLLER_BUTTON_Y
GAMEPAD_BUTTON_BACK: int = pygame.CONTROLLER_BUTTON_BACK
GAMEPAD_BUTTON_GUIDE: int = pygame.CONTROLLER_BUTTON_GUIDE
GAMEPAD_BUTTON_START: int = pygame.CONTROLLER_BUTTON_START
GAMEPAD_BUTTON_LEFTSTICK: int = pygame.CONTROLLER_BUTTON_LEFTSTICK
GAMEPAD_BUTTON_RIGHTSTICK: int = pygame.CONTROLLER_BUTTON_RIGHTSTICK
GAMEPAD_BUTTON_LEFTSHOULDER: int = pygame.CONTROLLER_BUTTON_LEFTSHOULDER
GAMEPAD_BUTTON_RIGHTSHOULDER: int = pygame.CONTROLLER_BUTTON_RIGHTSHOULDER
GAMEPAD_BUTTON_DPAD_UP: int = pygame.CONTROLLER_BUTTON_DPAD_UP
GAMEPAD_BUTTON_DPAD_DOWN: int = pygame.CONTROLLER_BUTTON_DPAD_DOWN
GAMEPAD_BUTTON_DPAD_LEFT: int = pygame.CONTROLLER_BUTTON_DPAD_LEFT
GAMEPAD_BUTTON_DPAD_RIGHT: int = pygame.CONTROLLER_BUTTON_DPAD_RIGHT
GAMEPAD_AXIS_LEFT_X: int = pygame.CONTROLLER_AXIS_LEFTX
GAMEPAD_AXIS_LEFT_Y: int = pygame.CONTROLLER_AXIS_LEFTY
GAMEPAD_AXIS_RIGHT_X: int = pygame.CONTROLLER_AXIS_RIGHTX
GAMEPAD_AXIS_RIGHT_Y: int = pygame.CONTROLLER_AXIS_RIGHTY
GAMEPAD_AXIS_TRIGGER_LEFT: int = pygame.CONTROLLER_AXIS_TRIGGERLEFT
GAMEPAD_AXIS_TRIGGER_RIGHT: int = pygame.CONTROLLER_AXIS_TRIGGERRIGHT
[docs]
def apply_deadzone(value: float, threshold: float = 0.15) -> float:
"""
Analog sticks rarely rest at exactly 0.0 (a bit of drift is
normal), so a raw GamepadAxisData.value is usually run through
this before acting on it.
:param value: A raw axis value, typically GamepadAxisData.value.
:param threshold: Values whose magnitude is at or below this are snapped to 0.0. The default value is 0.15.
:returns: 0.0 if abs(value) <= threshold, otherwise value unchanged.
"""
return 0.0 if abs(value) <= threshold else value