Module botroyale.gui.kex.widgets.input_manager
InputManager.
Characters representing the modifier keys:
^Control!Alt+Shift#Super
Expand source code Browse git
"""InputManager.
Characters representing the modifier keys:
- `^` Control
- `!` Alt
- `+` Shift
- `#` Super
"""
from typing import Callable, Union, TypeVar
from collections import defaultdict
from dataclasses import dataclass, field
from .. import kivy as kv
from ..util import (
XWidget,
restart_script,
_ping,
_pong,
)
KEYCODE_TEXT = {v: k for k, v in kv.Keyboard.keycodes.items()}
KeysFormat = TypeVar("KeysFormat", bound=str)
"""A type alias for a string formatted as either: `f'{modifiers} {key}'` or `key`."""
MODIFIER_SORT = "^!+#"
MOD2KEY = {
"ctrl": "^",
"alt-gr": "!",
"alt": "!",
"shift": "+",
"super": "#",
"meta": "#",
"control": "^",
"lctrl": "^",
"rctrl": "^",
"lalt": "!",
"ralt": "!",
"lshift": "+",
"rshift": "+",
"numlock": "",
"capslock": "",
}
KEY2MOD = {
"^": "ctrl",
"!": "alt",
"+": "shift",
"#": "super",
}
@dataclass(frozen=True, eq=True)
class KeyControl:
"""Represents a control for the `XInputManager`."""
name: str
"""The name of this control (to be used for filtering)."""
callback: Callable[[], None] = field(compare=False, repr=False)
"""Function to call when this control is invoked."""
keys: list[KeysFormat] = field(compare=False)
"""The keybind of this control."""
allow_repeat: bool = field(default=False, compare=False, repr=True)
"""Allow this control to be repeatedly invoked while holding down the keys."""
def _format_keys(
modifiers: list[str],
key_name: str,
honor_numlock: bool = True,
) -> KeysFormat:
"""Convert a combination of keys to a standard string format."""
if (
honor_numlock
and "numlock" in modifiers
and key_name.startswith("numpad")
and len(key_name) == 7
):
key_name = key_name[-1]
# Remove duplicate modifiers
modifiers = set(MOD2KEY[mod] for mod in modifiers)
modifiers -= {""}
# Remove modifier if it is the main key being pressed
# e.g. when key_name == "lctrl", "ctrl" will be in modifiers
if key_name in MOD2KEY:
modifiers -= {MOD2KEY[key_name]}
# No space required if no modifiers
if len(modifiers) == 0:
return key_name
# Order of modifiers should be consistent
sorted_modifiers = sorted(modifiers, key=lambda x: MODIFIER_SORT.index(x))
# Return the KeysFormat
mod_str = "".join(sorted_modifiers)
return f"{mod_str} {key_name}"
def humanize_keys(keys: KeysFormat) -> str:
"""Return a more human-readable string from a KeysFormat."""
mods, key = keys.split(" ") if " " in keys else ([], keys)
dstr = [KEY2MOD[mod] for mod in mods]
dstr.append(key)
return " + ".join(dstr)
class XInputManager(XWidget, kv.Widget):
"""See module documentation for details."""
# Set negative min_cooldown to disable repeated invoking
active = kv.BooleanProperty(True)
"""If the InputManager is active."""
log_press = kv.BooleanProperty(False)
"""If key presses should be logged."""
log_release = kv.BooleanProperty(False)
"""If key released should be logged."""
log_callback = kv.BooleanProperty(False)
"""If invocations should be logged."""
pressed = kv.StringProperty(" ")
"""Last keys that were pressed."""
released = kv.StringProperty(" ")
"""Last keys that were released."""
min_cooldown = kv.NumericProperty(0)
"""Minimum cooldown in milliseconds between invocations.
This will ultimately be limited by the system's "repeat rate" of the
keyboard. Setting a negative value will disable repeating.
"""
honor_numlock = kv.BooleanProperty(True)
"""Consider numpad keys as different than number keys when numlock is disabled."""
allow_overwrite = kv.BooleanProperty(False)
"""Allow overwriting existing controls."""
humanize = humanize_keys
"""Alias for `humanize_keys`."""
def __init__(
self,
name: str = "Unnamed",
default_controls: bool = True,
logger: Callable[[str], None] = print,
**kwargs,
):
"""Class for managing key press bindings and hotkeys.
Args:
name: Arbitrary name of the object. Used for debugging.
default_controls: If True, will call `XInputManager.register_defaults`.
logger: Function to be used for logging.
"""
self.name = name
self.controls = {}
self.control_keys = defaultdict(set)
super().__init__(**kwargs)
self.__last_down_ping = _ping() - self.min_cooldown
self.logger = logger
kv.Window.bind(on_key_down=self._on_key_down, on_key_up=self._on_key_up)
if default_controls:
self.register_defaults()
# Control management
def register(
self,
name: str,
callback: Callable[[], None],
keys: Union[KeysFormat, list[KeysFormat]],
allow_repeat: bool = False,
):
"""Register or modify a control.
Args:
name: Control name.
callback: Function to call when the control is invoked.
keys: Keypresses that will invoke the control.
allow_repeat: Allow this control to be repeatedly invoked if the
keys have not been released.
"""
keys = [keys] if isinstance(keys, str) else keys
kc = KeyControl(name, callback, keys, allow_repeat)
if kc.name in self.controls:
if not self.allow_overwrite:
raise ValueError(
f"{kc.name} already exists, enable allow_overwrite "
"or use a unique name."
)
old_kc = self.controls[kc.name]
self.logger(f"Replacing {old_kc} -> {kc}")
self._remove_kc(old_kc)
else:
self.logger(f"Registering {kc}")
self._register_kc(kc)
def remove(self, name: str):
"""Remove a control by *name*."""
if name not in self.controls:
self.logger(f"{self} cannot remove non-existant control: {name}")
return
kc = self.controls[name]
self.logger(f"Removing {kc}")
self._remove_kc(kc)
def remove_all(self):
"""Remove all controls."""
self.controls = {}
self.control_keys = defaultdict(set)
def _register_kc(self, kc: KeyControl):
self.controls[kc.name] = kc
for kf in kc.keys:
self.control_keys[kf].add(kc)
def _remove_kc(self, kc: KeyControl):
for kf in kc.keys:
self.control_keys[kf].remove(kc)
del self.controls[kc.name]
def register_defaults(self):
"""Register default controls (quit and restart)."""
self.register("app.quit", lambda: quit(), "^+ q")
self.register("app.restart", restart_script, "^+ w")
# Properties
@property
def humanized(self) -> str:
"""Return a more human-readable string of last pressed keys."""
return humanize_keys(self.pressed)
@property
def on_cooldown(self) -> bool:
"""Check if we are on cooldown for repeating a key down event."""
if self.min_cooldown < 0:
return True
return _pong(self.__last_down_ping) < self.min_cooldown
@property
def pressed_key(self) -> str:
"""The last pressed key."""
return self.pressed.split(" ")[1] if " " in self.pressed else self.pressed
@property
def pressed_mods(self) -> str:
"""The last pressed modifiers."""
kf = self.pressed
if " " in kf:
mods, key = kf.split(" ")
mods = list(mods)
else:
mods, key = [], kf
if key in MOD2KEY:
extra_mod = MOD2KEY[key]
if extra_mod not in mods:
mods.append(extra_mod)
sorted_mods = sorted(mods, key=lambda x: MODIFIER_SORT.index(x))
return "".join(sorted_mods)
def __repr__(self):
"""Repr."""
cooldown = (
f" {self.min_cooldown}ms cd" if self.min_cooldown >= 0 else " no repeat"
)
active = "" if self.active else " INACTIVE"
return (
f"<{self.name} InputManager, {len(self.controls)} controls "
f"on {len(self.control_keys)} hotkeys{cooldown} {active}>"
)
@property
def _debug_str(self):
strs = [
f"{self}",
"Keys:",
]
for kf, kcs in self.control_keys.items():
strs.extend(f"{kf:>15} : {kc}" for kc in kcs)
strs.extend(
[
"Controls:",
*(f" {kc}" for kc in self.controls.values()),
]
)
return "\n".join(strs)
# Kivy key press management
def _on_key_down(
self,
window,
key: int,
scancode: int,
codepoint: str,
modifiers: list[str],
):
if not self.active or self._app_blocked:
return
key_name = KEYCODE_TEXT.get(key, "")
kf = _format_keys(modifiers, key_name, self.honor_numlock)
is_repeat = kf == self.pressed
if is_repeat and self.on_cooldown:
return
self.__last_down_ping = _ping()
if not is_repeat and self.log_press:
self.logger(f"Pressed: |{kf}| {self}")
if kf in self.control_keys:
for kc in self.control_keys[kf]:
if is_repeat and not kc.allow_repeat:
continue
self._invoke_kc(kc)
self.pressed = kf
def _on_key_up(self, window, key: int, scancode: int):
self.released = self.pressed
self.pressed = " "
if not self.active or self._app_blocked:
return
if self.log_release:
key_name = KEYCODE_TEXT.get(key, "")
self.logger(f"Released: |{key_name}| {self}")
def _invoke_kc(self, kc: KeyControl):
callback = kc.callback
if self.log_callback:
self.logger(f"Invoking {kc} {callback=}")
callback()
@property
def _app_blocked(self):
if self.app is not None:
return self.app.block_input
return False
Global variables
var KeysFormat-
A type alias for a string formatted as either:
f'{modifiers} {key}'orkey.
Functions
def humanize_keys(keys: ~KeysFormat) ‑> str-
Return a more human-readable string from a KeysFormat.
Expand source code Browse git
def humanize_keys(keys: KeysFormat) -> str: """Return a more human-readable string from a KeysFormat.""" mods, key = keys.split(" ") if " " in keys else ([], keys) dstr = [KEY2MOD[mod] for mod in mods] dstr.append(key) return " + ".join(dstr)
Classes
class KeyControl (name: str, callback: Callable[[], None], keys: list[~KeysFormat], allow_repeat: bool = False)-
Represents a control for the
XInputManager.Expand source code Browse git
@dataclass(frozen=True, eq=True) class KeyControl: """Represents a control for the `XInputManager`.""" name: str """The name of this control (to be used for filtering).""" callback: Callable[[], None] = field(compare=False, repr=False) """Function to call when this control is invoked.""" keys: list[KeysFormat] = field(compare=False) """The keybind of this control.""" allow_repeat: bool = field(default=False, compare=False, repr=True) """Allow this control to be repeatedly invoked while holding down the keys."""Class variables
var allow_repeat : bool-
Allow this control to be repeatedly invoked while holding down the keys.
var callback : Callable[[], None]-
Function to call when this control is invoked.
var keys : list[~KeysFormat]-
The keybind of this control.
var name : str-
The name of this control (to be used for filtering).
class XInputManager (name: str = 'Unnamed', default_controls: bool = True, logger: Callable[[str], None] = <built-in function print>, **kwargs)-
See module documentation for details.
Class for managing key press bindings and hotkeys.
- Args
- -----=
name- Arbitrary name of the object. Used for debugging.
default_controls- If True, will call
XInputManager.register_defaults(). logger- Function to be used for logging.
Expand source code Browse git
class XInputManager(XWidget, kv.Widget): """See module documentation for details.""" # Set negative min_cooldown to disable repeated invoking active = kv.BooleanProperty(True) """If the InputManager is active.""" log_press = kv.BooleanProperty(False) """If key presses should be logged.""" log_release = kv.BooleanProperty(False) """If key released should be logged.""" log_callback = kv.BooleanProperty(False) """If invocations should be logged.""" pressed = kv.StringProperty(" ") """Last keys that were pressed.""" released = kv.StringProperty(" ") """Last keys that were released.""" min_cooldown = kv.NumericProperty(0) """Minimum cooldown in milliseconds between invocations. This will ultimately be limited by the system's "repeat rate" of the keyboard. Setting a negative value will disable repeating. """ honor_numlock = kv.BooleanProperty(True) """Consider numpad keys as different than number keys when numlock is disabled.""" allow_overwrite = kv.BooleanProperty(False) """Allow overwriting existing controls.""" humanize = humanize_keys """Alias for `humanize_keys`.""" def __init__( self, name: str = "Unnamed", default_controls: bool = True, logger: Callable[[str], None] = print, **kwargs, ): """Class for managing key press bindings and hotkeys. Args: name: Arbitrary name of the object. Used for debugging. default_controls: If True, will call `XInputManager.register_defaults`. logger: Function to be used for logging. """ self.name = name self.controls = {} self.control_keys = defaultdict(set) super().__init__(**kwargs) self.__last_down_ping = _ping() - self.min_cooldown self.logger = logger kv.Window.bind(on_key_down=self._on_key_down, on_key_up=self._on_key_up) if default_controls: self.register_defaults() # Control management def register( self, name: str, callback: Callable[[], None], keys: Union[KeysFormat, list[KeysFormat]], allow_repeat: bool = False, ): """Register or modify a control. Args: name: Control name. callback: Function to call when the control is invoked. keys: Keypresses that will invoke the control. allow_repeat: Allow this control to be repeatedly invoked if the keys have not been released. """ keys = [keys] if isinstance(keys, str) else keys kc = KeyControl(name, callback, keys, allow_repeat) if kc.name in self.controls: if not self.allow_overwrite: raise ValueError( f"{kc.name} already exists, enable allow_overwrite " "or use a unique name." ) old_kc = self.controls[kc.name] self.logger(f"Replacing {old_kc} -> {kc}") self._remove_kc(old_kc) else: self.logger(f"Registering {kc}") self._register_kc(kc) def remove(self, name: str): """Remove a control by *name*.""" if name not in self.controls: self.logger(f"{self} cannot remove non-existant control: {name}") return kc = self.controls[name] self.logger(f"Removing {kc}") self._remove_kc(kc) def remove_all(self): """Remove all controls.""" self.controls = {} self.control_keys = defaultdict(set) def _register_kc(self, kc: KeyControl): self.controls[kc.name] = kc for kf in kc.keys: self.control_keys[kf].add(kc) def _remove_kc(self, kc: KeyControl): for kf in kc.keys: self.control_keys[kf].remove(kc) del self.controls[kc.name] def register_defaults(self): """Register default controls (quit and restart).""" self.register("app.quit", lambda: quit(), "^+ q") self.register("app.restart", restart_script, "^+ w") # Properties @property def humanized(self) -> str: """Return a more human-readable string of last pressed keys.""" return humanize_keys(self.pressed) @property def on_cooldown(self) -> bool: """Check if we are on cooldown for repeating a key down event.""" if self.min_cooldown < 0: return True return _pong(self.__last_down_ping) < self.min_cooldown @property def pressed_key(self) -> str: """The last pressed key.""" return self.pressed.split(" ")[1] if " " in self.pressed else self.pressed @property def pressed_mods(self) -> str: """The last pressed modifiers.""" kf = self.pressed if " " in kf: mods, key = kf.split(" ") mods = list(mods) else: mods, key = [], kf if key in MOD2KEY: extra_mod = MOD2KEY[key] if extra_mod not in mods: mods.append(extra_mod) sorted_mods = sorted(mods, key=lambda x: MODIFIER_SORT.index(x)) return "".join(sorted_mods) def __repr__(self): """Repr.""" cooldown = ( f" {self.min_cooldown}ms cd" if self.min_cooldown >= 0 else " no repeat" ) active = "" if self.active else " INACTIVE" return ( f"<{self.name} InputManager, {len(self.controls)} controls " f"on {len(self.control_keys)} hotkeys{cooldown} {active}>" ) @property def _debug_str(self): strs = [ f"{self}", "Keys:", ] for kf, kcs in self.control_keys.items(): strs.extend(f"{kf:>15} : {kc}" for kc in kcs) strs.extend( [ "Controls:", *(f" {kc}" for kc in self.controls.values()), ] ) return "\n".join(strs) # Kivy key press management def _on_key_down( self, window, key: int, scancode: int, codepoint: str, modifiers: list[str], ): if not self.active or self._app_blocked: return key_name = KEYCODE_TEXT.get(key, "") kf = _format_keys(modifiers, key_name, self.honor_numlock) is_repeat = kf == self.pressed if is_repeat and self.on_cooldown: return self.__last_down_ping = _ping() if not is_repeat and self.log_press: self.logger(f"Pressed: |{kf}| {self}") if kf in self.control_keys: for kc in self.control_keys[kf]: if is_repeat and not kc.allow_repeat: continue self._invoke_kc(kc) self.pressed = kf def _on_key_up(self, window, key: int, scancode: int): self.released = self.pressed self.pressed = " " if not self.active or self._app_blocked: return if self.log_release: key_name = KEYCODE_TEXT.get(key, "") self.logger(f"Released: |{key_name}| {self}") def _invoke_kc(self, kc: KeyControl): callback = kc.callback if self.log_callback: self.logger(f"Invoking {kc} {callback=}") callback() @property def _app_blocked(self): if self.app is not None: return self.app.block_input return FalseAncestors
- XWidget
- kivy.uix.widget.Widget
- kivy.uix.widget.WidgetBase
- kivy._event.EventDispatcher
- kivy._event.ObjectWithUid
Instance variables
var active-
If the InputManager is active.
var allow_overwrite-
Allow overwriting existing controls.
var honor_numlock-
Consider numpad keys as different than number keys when numlock is disabled.
var humanized : str-
Return a more human-readable string of last pressed keys.
Expand source code Browse git
@property def humanized(self) -> str: """Return a more human-readable string of last pressed keys.""" return humanize_keys(self.pressed) var log_callback-
If invocations should be logged.
var log_press-
If key presses should be logged.
var log_release-
If key released should be logged.
var min_cooldown-
Minimum cooldown in milliseconds between invocations.
This will ultimately be limited by the system's "repeat rate" of the keyboard. Setting a negative value will disable repeating.
var on_cooldown : bool-
Check if we are on cooldown for repeating a key down event.
Expand source code Browse git
@property def on_cooldown(self) -> bool: """Check if we are on cooldown for repeating a key down event.""" if self.min_cooldown < 0: return True return _pong(self.__last_down_ping) < self.min_cooldown var pressed-
Last keys that were pressed.
var pressed_key : str-
The last pressed key.
Expand source code Browse git
@property def pressed_key(self) -> str: """The last pressed key.""" return self.pressed.split(" ")[1] if " " in self.pressed else self.pressed var pressed_mods : str-
The last pressed modifiers.
Expand source code Browse git
@property def pressed_mods(self) -> str: """The last pressed modifiers.""" kf = self.pressed if " " in kf: mods, key = kf.split(" ") mods = list(mods) else: mods, key = [], kf if key in MOD2KEY: extra_mod = MOD2KEY[key] if extra_mod not in mods: mods.append(extra_mod) sorted_mods = sorted(mods, key=lambda x: MODIFIER_SORT.index(x)) return "".join(sorted_mods) var released-
Last keys that were released.
Methods
def humanize(keys: ~KeysFormat) ‑> str-
Return a more human-readable string from a KeysFormat.
Expand source code Browse git
def humanize_keys(keys: KeysFormat) -> str: """Return a more human-readable string from a KeysFormat.""" mods, key = keys.split(" ") if " " in keys else ([], keys) dstr = [KEY2MOD[mod] for mod in mods] dstr.append(key) return " + ".join(dstr) def register(self, name: str, callback: Callable[[], None], keys: Union[~KeysFormat, list[~KeysFormat]], allow_repeat: bool = False)-
Register or modify a control.
- Args
- -----=
name- Control name.
callback- Function to call when the control is invoked.
keys- Keypresses that will invoke the control.
allow_repeat- Allow this control to be repeatedly invoked if the keys have not been released.
Expand source code Browse git
def register( self, name: str, callback: Callable[[], None], keys: Union[KeysFormat, list[KeysFormat]], allow_repeat: bool = False, ): """Register or modify a control. Args: name: Control name. callback: Function to call when the control is invoked. keys: Keypresses that will invoke the control. allow_repeat: Allow this control to be repeatedly invoked if the keys have not been released. """ keys = [keys] if isinstance(keys, str) else keys kc = KeyControl(name, callback, keys, allow_repeat) if kc.name in self.controls: if not self.allow_overwrite: raise ValueError( f"{kc.name} already exists, enable allow_overwrite " "or use a unique name." ) old_kc = self.controls[kc.name] self.logger(f"Replacing {old_kc} -> {kc}") self._remove_kc(old_kc) else: self.logger(f"Registering {kc}") self._register_kc(kc) def register_defaults(self)-
Register default controls (quit and restart).
Expand source code Browse git
def register_defaults(self): """Register default controls (quit and restart).""" self.register("app.quit", lambda: quit(), "^+ q") self.register("app.restart", restart_script, "^+ w") def remove(self, name: str)-
Remove a control by name.
Expand source code Browse git
def remove(self, name: str): """Remove a control by *name*.""" if name not in self.controls: self.logger(f"{self} cannot remove non-existant control: {name}") return kc = self.controls[name] self.logger(f"Removing {kc}") self._remove_kc(kc) def remove_all(self)-
Remove all controls.
Expand source code Browse git
def remove_all(self): """Remove all controls.""" self.controls = {} self.control_keys = defaultdict(set)
Inherited members