# Orca # # Copyright 2005-2008 Sun Microsystems Inc. # Copyright 2011-2016 Igalia, S.L. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # pylint: disable=broad-exception-caught # pylint: disable=wrong-import-position # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes """Provides support for handling input events.""" # This has to be the first non-docstring line in the module to make linters happy. from __future__ import annotations __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ "Copyright (c) 2011-2016 Igalia, S.L." __license__ = "LGPL" import inspect import math import time import unicodedata from typing import Callable, TYPE_CHECKING import gi gi.require_version("Atspi", "2.0") gi.require_version("Gdk", "3.0") from gi.repository import Atspi from gi.repository import Gdk from gi.repository import GLib from . import debug from . import focus_manager from . import keybindings from . import keynames from . import messages from . import orca_modifier_manager from . import script_manager from . import settings from .ax_utilities import AXUtilities if TYPE_CHECKING: from .scripts import default KEYBOARD_EVENT = "keyboard" BRAILLE_EVENT = "braille" MOUSE_BUTTON_EVENT = "mouse:button" REMOTE_CONTROLLER_EVENT = "remote controller" class InputEvent: """Provides support for handling input events.""" def __init__(self, event_type: str) -> None: """Creates a new KEYBOARD_EVENT, BRAILLE_EVENT, or MOUSE_BUTTON_EVENT.""" self.type: str = event_type self.time: float = time.time() self._click_count: int = 0 def get_click_count(self) -> int: """Return the count of the number of clicks a user has made.""" return self._click_count def set_click_count(self, count: int) -> None: """Updates the count of the number of clicks a user has made.""" self._click_count = count def as_single_line_string(self) -> str: """Returns a single-line string representation of this event.""" return f"{self.type}" class KeyboardEvent(InputEvent): """Provides support for handling keyboard events.""" # pylint:disable=too-many-arguments # pylint:disable=too-many-positional-arguments def __init__( self, pressed: bool, keycode: int, keysym: int, modifiers: int, text: str ) -> None: """Creates a new InputEvent of type KEYBOARD_EVENT. Arguments: - pressed: True if this is a key press, False for a release. - keycode: the hardware keycode. - keysym: the translated keysym. - modifiers: a bitflag giving the active modifiers. - text: the text that would be inserted if this key is pressed. """ super().__init__(KEYBOARD_EVENT) self.id: int = keysym self.type: Atspi.EventType = ( Atspi.EventType.KEY_PRESSED_EVENT if pressed else Atspi.EventType.KEY_RELEASED_EVENT ) self.hw_code: int = keycode self._text: str = text self.modifiers: int = modifiers & Gdk.ModifierType.MODIFIER_MASK # pylint: disable=no-member if modifiers & (1 << Atspi.ModifierType.NUMLOCK): self.modifiers |= (1 << Atspi.ModifierType.NUMLOCK) self.keyval_name: str = Gdk.keyval_name(keysym) or "" self.timestamp: float = time.time() self._script: default.Script | None = None self._window: Atspi.Accessible | None = None self._obj: Atspi.Accessible | None = None self._handler: InputEventHandler | None = None self._consumer: Callable[..., bool] | None = None self._is_kp_with_numlock: bool = False # Some implementors don't include numlock in the modifiers. Unfortunately, # trying to heuristically hack around this just by looking at the event # is not reliable. Ditto regarding asking Gdk for the numlock state. if self.keyval_name.startswith("KP"): if self.modifiers & (1 << Atspi.ModifierType.NUMLOCK): self._is_kp_with_numlock = True modifier_manager = orca_modifier_manager.get_manager() if self.is_orca_modifier(): modifier_manager.set_pressed_state(pressed) if modifier_manager.get_pressed_state(): self.modifiers |= keybindings.ORCA_MODIFIER_MASK # pylint:enable=too-many-arguments # pylint:enable=too-many-positional-arguments def __eq__(self, other: object) -> bool: if not isinstance(other, KeyboardEvent): return False if self.type == other.type and self.hw_code == other.hw_code: return self.timestamp == other.timestamp return False def __str__(self) -> str: if self.should_obscure(): keyid = hw_code = modifiers = text = keyval_name = "*" else: keyid = str(self.id) hw_code = str(self.hw_code) modifiers = str(self.modifiers) keyval_name = self.keyval_name text = self._text return f"KEYBOARD_EVENT: type={self.type.value_name.upper()}\n" \ + f" id={keyid}\n" \ + f" hw_code={hw_code}\n" \ + f" modifiers={modifiers}\n" \ + f" text='{text}'\n" \ + f" keyval_name='{keyval_name}'\n" \ + f" timestamp={self.timestamp}\n" \ + f" clickCount={self._click_count}" def as_single_line_string(self) -> str: """Returns a single-line string representation of this event.""" if self.should_obscure(): return "(obscured)" return ( f"'{self.keyval_name}' ({self.hw_code}) mods: {self.modifiers} " f"{self.type.value_nick}" ) def is_alt_control_or_orca_modified(self) -> bool: """Return True if this key is Alt, Control, or Orca modified.""" if self.modifiers & keybindings.CTRL_MODIFIER_MASK \ or self.modifiers & keybindings.ALT_MODIFIER_MASK \ or self.modifiers & keybindings.ORCA_MODIFIER_MASK: return True return False def should_obscure(self) -> bool: """Returns True if we should obscure the details of this event.""" if not AXUtilities.is_password_text(self._obj): return False if not self.is_printable_key() and not self.is_space(): return False if self.is_alt_control_or_orca_modified(): return False return True def is_navigation_key(self) -> bool: """Return True if this is a navigation key.""" keys = [ Gdk.KEY_Down, Gdk.KEY_End, Gdk.KEY_Home, Gdk.KEY_Left, Gdk.KEY_Right, Gdk.KEY_Up, ] return self.id in keys def is_action_key(self) -> bool: """Return True if this is an action key.""" keys = [ Gdk.KEY_BackSpace, Gdk.KEY_Delete, Gdk.KEY_Escape, Gdk.KEY_KP_Enter, Gdk.KEY_Page_Down, Gdk.KEY_Page_Up, Gdk.KEY_Return, Gdk.KEY_Tab, ] return self.id in keys def is_alphabetic_key(self) -> bool: """Return True if this is an alphabetic key.""" name = self.get_key_name() if not len(name) == 1: return False return name.isalpha() def is_diacritical_key(self) -> bool: """Return True if this is a non-spacing diacritical key.""" keys = [ Gdk.KEY_dead_A, Gdk.KEY_dead_a, Gdk.KEY_dead_abovecomma, Gdk.KEY_dead_abovedot, Gdk.KEY_dead_abovereversedcomma, Gdk.KEY_dead_abovering, Gdk.KEY_dead_aboveverticalline, Gdk.KEY_dead_acute, Gdk.KEY_dead_belowbreve, Gdk.KEY_dead_belowcircumflex, Gdk.KEY_dead_belowcomma, Gdk.KEY_dead_belowdiaeresis, Gdk.KEY_dead_belowdot, Gdk.KEY_dead_belowmacron, Gdk.KEY_dead_belowring, Gdk.KEY_dead_belowtilde, Gdk.KEY_dead_belowverticalline, Gdk.KEY_dead_breve, Gdk.KEY_dead_capital_schwa, Gdk.KEY_dead_caron, Gdk.KEY_dead_cedilla, Gdk.KEY_dead_circumflex, Gdk.KEY_dead_currency, Gdk.KEY_dead_dasia, Gdk.KEY_dead_diaeresis, Gdk.KEY_dead_doubleacute, Gdk.KEY_dead_doublegrave, Gdk.KEY_dead_E, Gdk.KEY_dead_e, Gdk.KEY_dead_grave, Gdk.KEY_dead_greek, Gdk.KEY_dead_hook, Gdk.KEY_dead_horn, Gdk.KEY_dead_I, Gdk.KEY_dead_i, Gdk.KEY_dead_invertedbreve, Gdk.KEY_dead_iota, Gdk.KEY_dead_longsolidusoverlay, Gdk.KEY_dead_lowline, Gdk.KEY_dead_macron, Gdk.KEY_dead_O, Gdk.KEY_dead_o, Gdk.KEY_dead_ogonek, Gdk.KEY_dead_perispomeni, Gdk.KEY_dead_psili, Gdk.KEY_dead_semivoiced_sound, Gdk.KEY_dead_small_schwa, Gdk.KEY_dead_stroke, Gdk.KEY_dead_tilde, Gdk.KEY_dead_U, Gdk.KEY_dead_u, Gdk.KEY_dead_voiced_sound, ] if self.id in keys: return True name = self.get_key_name() if len(name) == 1: category = unicodedata.category(name) # Mn = Mark, nonspacing; Mc = Mark, spacing combining; Me = Mark, enclosing if category in ("Mn", "Mc", "Me"): return True return False def is_function_key(self) -> bool: """Return True if this is a function key.""" keys = [ Gdk.KEY_F1, Gdk.KEY_F2, Gdk.KEY_F3, Gdk.KEY_F4, Gdk.KEY_F5, Gdk.KEY_F6, Gdk.KEY_F7, Gdk.KEY_F8, Gdk.KEY_F9, Gdk.KEY_F10, Gdk.KEY_F11, Gdk.KEY_F12, ] return self.id in keys def is_locking_key(self) -> bool: """Return True if this is a locking key.""" if self.is_orca_modifier(): return self._click_count == 2 keys = [ Gdk.KEY_Caps_Lock, Gdk.KEY_Num_Lock, Gdk.KEY_Scroll_Lock, Gdk.KEY_Shift_Lock, ] return self.id in keys def is_modifier_key(self) -> bool: """Return True if this is a modifier key.""" keys = [ Gdk.KEY_Alt_L, Gdk.KEY_Alt_R, Gdk.KEY_Control_L, Gdk.KEY_Control_R, Gdk.KEY_Meta_L, Gdk.KEY_Meta_R, Gdk.KEY_Shift_L, Gdk.KEY_Shift_R, Gdk.KEY_ISO_Level3_Shift, ] return self.id in keys or self.is_orca_modifier() def is_numeric_key(self) -> bool: """Return True if this is a numeric key.""" keys = [ Gdk.KEY_0, Gdk.KEY_1, Gdk.KEY_2, Gdk.KEY_3, Gdk.KEY_4, Gdk.KEY_5, Gdk.KEY_6, Gdk.KEY_7, Gdk.KEY_8, Gdk.KEY_9, Gdk.KEY_KP_0, Gdk.KEY_KP_1, Gdk.KEY_KP_2, Gdk.KEY_KP_3, Gdk.KEY_KP_4, Gdk.KEY_KP_5, Gdk.KEY_KP_6, Gdk.KEY_KP_7, Gdk.KEY_KP_8, Gdk.KEY_KP_9, ] return self.id in keys def is_orca_modifier(self) -> bool: """Return True if this is the Orca modifier key.""" if self.id == Gdk.KEY_KP_0 and self.modifiers & keybindings.SHIFT_MODIFIER_MASK: return orca_modifier_manager.get_manager().is_orca_modifier("KP_Insert") return orca_modifier_manager.get_manager().is_orca_modifier(self.keyval_name) def is_orca_modified(self) -> bool: """Return True if this key is Orca modified.""" if self.is_orca_modifier(): return False return bool(self.modifiers & keybindings.ORCA_MODIFIER_MASK) def is_keypad_key_with_numlock_on(self) -> bool: """Return True if this is a key pad key with numlock on.""" return self._is_kp_with_numlock def is_printable_key(self) -> bool: """Return True if this is a printable key.""" name = self.get_key_name() if not len(name) == 1: return False return name.isprintable() def is_pressed_key(self) -> bool: """Returns True if the key is pressed""" return self.type == Atspi.EventType.KEY_PRESSED_EVENT def is_punctuation_key(self) -> bool: """Return True if this is a punctuation key.""" keys = [ Gdk.KEY_acute, Gdk.KEY_ampersand, Gdk.KEY_apostrophe, Gdk.KEY_asciicircum, Gdk.KEY_asciitilde, Gdk.KEY_asterisk, Gdk.KEY_at, Gdk.KEY_backslash, Gdk.KEY_bar, Gdk.KEY_braceleft, Gdk.KEY_braceright, Gdk.KEY_bracketleft, Gdk.KEY_bracketright, Gdk.KEY_brokenbar, Gdk.KEY_cedilla, Gdk.KEY_cent, Gdk.KEY_colon, Gdk.KEY_comma, Gdk.KEY_copyright, Gdk.KEY_currency, Gdk.KEY_degree, Gdk.KEY_diaeresis, Gdk.KEY_dollar, Gdk.KEY_EuroSign, Gdk.KEY_equal, Gdk.KEY_exclam, Gdk.KEY_exclamdown, Gdk.KEY_grave, Gdk.KEY_greater, Gdk.KEY_guillemotleft, Gdk.KEY_guillemotright, Gdk.KEY_hyphen, Gdk.KEY_KP_Decimal, Gdk.KEY_KP_Add, Gdk.KEY_KP_Divide, Gdk.KEY_KP_Multiply, Gdk.KEY_KP_Subtract, Gdk.KEY_less, Gdk.KEY_macron, Gdk.KEY_minus, Gdk.KEY_notsign, Gdk.KEY_numbersign, Gdk.KEY_paragraph, Gdk.KEY_parenleft, Gdk.KEY_parenright, Gdk.KEY_percent, Gdk.KEY_period, Gdk.KEY_periodcentered, Gdk.KEY_plus, Gdk.KEY_plusminus, Gdk.KEY_question, Gdk.KEY_questiondown, Gdk.KEY_quotedbl, Gdk.KEY_quoteleft, Gdk.KEY_quoteright, Gdk.KEY_registered, Gdk.KEY_section, Gdk.KEY_semicolon, Gdk.KEY_slash, Gdk.KEY_sterling, Gdk.KEY_underscore, Gdk.KEY_yen, ] return self.id in keys def is_space(self) -> bool: """Return True if this is the space key.""" return self.id == Gdk.KEY_space def get_locking_state(self) -> bool | None: """Returns True if the event locked a locking key, False if the event unlocked it, and None if not a locking key.""" if not self.is_locking_key(): return None if self.id == Gdk.KEY_Caps_Lock: mod = Atspi.ModifierType.SHIFTLOCK elif self.id == Gdk.KEY_Shift_Lock: mod = Atspi.ModifierType.SHIFT elif self.id == Gdk.KEY_Num_Lock: mod = Atspi.ModifierType.NUMLOCK else: return None return not self.modifiers & (1 << mod) def get_locking_state_string(self) -> str: """Returns the string reflecting the locking state.""" locked = self.get_locking_state() if locked is None: return "" if not locked: return messages.LOCKING_KEY_STATE_OFF return messages.LOCKING_KEY_STATE_ON def get_key_name(self) -> str: """Returns the string to be used for presenting the key.""" if self._text.strip() and self._text.isprintable(): return self._text name = keynames.get_key_name(self.keyval_name) if name is not None: return name unicode_codepoint = Gdk.keyval_to_unicode(self.id) if unicode_codepoint: char = chr(unicode_codepoint) if char.isprintable(): return char return self.keyval_name def get_object(self) -> Atspi.Accessible | None: """Returns the object believed to be associated with this key event.""" return self._obj def set_object(self, obj: Atspi.Accessible | None) -> None: """Sets the object believed to be associated with this key event.""" module_name = inspect.getmodulename(inspect.stack()[1].filename) if not (module_name and module_name.startswith("input_event")): raise PermissionError("Unauthorized setter of input event property") self._obj = obj def get_window(self) -> Atspi.Accessible | None: """Returns the window believed to be associated with this key event.""" return self._window def set_window(self, window: Atspi.Accessible | None) -> None: """Sets the window believed to be associated with this key event.""" module_name = inspect.getmodulename(inspect.stack()[1].filename) if not (module_name and module_name.startswith("input_event")): raise PermissionError("Unauthorized setter of input event property") self._window = window def get_script(self) -> default.Script | None: """Returns the script believed to be associated with this key event.""" return self._script def set_script(self, script: default.Script | None) -> None: """Sets the script believed to be associated with this key event.""" module_name = inspect.getmodulename(inspect.stack()[1].filename) if not (module_name and module_name.startswith("input_event")): raise PermissionError("Unauthorized setter of input event property") self._script = script def get_handler(self) -> InputEventHandler | None: """Returns the handler associated with this key event.""" return self._handler def _get_user_handler(self) -> InputEventHandler | None: # TODO - JD: This should go away once plugin support is in place. try: bindings = settings.keyBindingsMap.get(self._script.__module__) except Exception: bindings = None if not bindings: try: bindings = settings.keyBindingsMap.get("default") except Exception: bindings = None if bindings is None: return None try: handler = bindings.get_input_handler(self) except Exception: handler = None return handler def _present(self) -> None: if not self._script: return if self.is_pressed_key(): self._script.interrupt_presentation() if self._script.get_learn_mode_presenter().is_active(): return self._script.present_keyboard_event(self) def process(self) -> None: """Processes this input event.""" start_time = time.time() should_obscure = self.should_obscure() if not should_obscure: data = f"'{self.keyval_name}' ({self.hw_code})" else: data = "(obscured)" debug.print_message(debug.LEVEL_INFO, f"\n{self}") msg = f'\nvvvvv PROCESS {self.type.value_name.upper()}: {data} vvvvv' debug.print_message(debug.LEVEL_INFO, msg, False) tokens = ["SCRIPT:", self._script] debug.print_tokens(debug.LEVEL_INFO, tokens, True) tokens = ["WINDOW:", self._window] debug.print_tokens(debug.LEVEL_INFO, tokens, True) tokens = ["LOCATION:", self._obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if self._script: self._handler = self._get_user_handler() \ or self._script.key_bindings.get_input_handler(self) if not should_obscure: tokens = ["HANDLER:", str(self._handler)] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if self._script.get_learn_mode_presenter().is_active(): self._consumer = self._script.get_learn_mode_presenter().handle_event tokens = ["CONSUMER:", str(self._consumer)] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if self.is_orca_modifier() and self._click_count == 2: orca_modifier_manager.get_manager().toggle_modifier(self) self._present() if self.is_pressed_key() and (self._consumer \ or (self._handler and self._handler.function is not None \ and self._handler.is_enabled())): GLib.timeout_add(1, self._consume) msg = f"TOTAL PROCESSING TIME: {time.time() - start_time:.4f}" debug.print_message(debug.LEVEL_INFO, msg, True) msg = f"^^^^^ PROCESS {self.type.value_name.upper()}: {data} ^^^^^\n" debug.print_message(debug.LEVEL_INFO, msg, False) def _consume(self) -> bool: """Consumes this input event after a timeout. Returns False to stop the timeout.""" start_time = time.time() data = f"'{self.keyval_name}' ({self.hw_code})" msg = f"\nvvvvv CONSUME {self.type.value_name.upper()}: {data} vvvvv" debug.print_message(debug.LEVEL_INFO, msg, False) if self._consumer: msg = f"KEYBOARD EVENT: Consumer is {self._consumer.__name__}" debug.print_message(debug.LEVEL_INFO, msg, True) self._consumer(self) elif self._handler and self._handler.function is not None and self._handler.is_enabled(): msg = f"KEYBOARD EVENT: Handler is {self._handler}" debug.print_message(debug.LEVEL_INFO, msg, True) try: self._handler.function(self._script, self) except GLib.GError as error: msg = f"KEYBOARD EVENT: Exception calling handler function: {error}" debug.print_message(debug.LEVEL_WARNING, msg, True) else: msg = "KEYBOARD EVENT: No enabled handler or consumer" debug.print_message(debug.LEVEL_INFO, msg, True) msg = f"TOTAL PROCESSING TIME: {time.time() - start_time:.4f}" debug.print_message(debug.LEVEL_INFO, msg, True) msg = f"^^^^^ CONSUME {self.type.value_name.upper()}: {data} ^^^^^\n" debug.print_message(debug.LEVEL_INFO, msg, False) return False class BrailleEvent(InputEvent): """Provides support for handling braille events.""" def __init__(self, event: dict) -> None: super().__init__(BRAILLE_EVENT) self.event: dict = event self._script: default.Script | None = script_manager.get_manager().get_active_script() def __str__(self) -> str: return f"{self.type.upper()} {self.event}" def get_handler(self) -> InputEventHandler | None: """Returns the handler associated with this event.""" try: assert self._script is not None except AssertionError: tokens = ["BRAILLE EVENT: No active script found for", self] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None command: str = self.event["command"] user_bindings: dict | None = None user_bindings_map: dict = settings.brailleBindingsMap if self._script.name in user_bindings_map: user_bindings = user_bindings_map[self._script.name] else: user_bindings = user_bindings_map.get("default") if user_bindings and command in user_bindings: handler: InputEventHandler | None = user_bindings[command] tokens = [f"BRAILLE EVENT: User handler for command {command} is", handler] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return handler handler = self._script.braille_bindings.get(command) tokens = [f"BRAILLE EVENT: Handler for command {command} is", handler] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return handler def process(self): """Processes this event.""" tokens = ["\nvvvvv PROCESS", self, "vvvvv"] debug.print_tokens(debug.LEVEL_INFO, tokens, False) start_time = time.time() result = self._process() msg = f"TOTAL PROCESSING TIME: {time.time() - start_time:.4f}" debug.print_message(debug.LEVEL_INFO, msg, False) tokens = ["^^^^^ PROCESS", self, "^^^^^"] debug.print_tokens(debug.LEVEL_INFO, tokens, False) return result def _process(self): handler = self.get_handler() if not handler: if self._script.get_learn_mode_presenter().is_active(): tokens = ["BRAILLE EVENT: Learn mode presenter handles", self] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True tokens = ["BRAILLE EVENT: No handler found for", self] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False if handler.function: tokens = ["BRAILLE EVENT: Handler is:", handler] debug.print_tokens(debug.LEVEL_INFO, tokens, True) handler.function(self._script, self) return True class MouseButtonEvent(InputEvent): """Provides support for handling mouse button events.""" # TODO - JD: Remove this and the validation logic once we have a fix for # https://gitlab.gnome.org/GNOME/at-spi2-core/-/issues/194. try: display = Gdk.Display.get_default() seat = Gdk.Display.get_default_seat(display) _pointer = seat.get_pointer() except Exception: _pointer = None def __init__(self, event): super().__init__(MOUSE_BUTTON_EVENT) self.x = event.detail1 self.y = event.detail2 self.pressed = event.type.endswith('p') self.button = event.type[len("mouse:button:"):-1] self._script = script_manager.get_manager().get_active_script() self.window = focus_manager.get_manager().get_active_window() self.app = None if self.pressed: self._validate_coordinates() if not self._script: return if not AXUtilities.can_be_active_window(self.window): self.window = AXUtilities.find_active_window() if not self.window: return self.app = AXUtilities.get_application(self.window) def _validate_coordinates(self): if not self._pointer: return x, y = self._pointer.get_position()[1:] if math.sqrt((self.x - x)**2 + (self.y - y)**2) < 25: return msg = ( f"WARNING: Event coordinates ({self.x}, {self.y}) may be bogus. " f"Updating to ({x}, {y})" ) debug.print_message(debug.LEVEL_INFO, msg, True) self.x, self.y = x, y class RemoteControllerEvent(InputEvent): """A simple input event whose main purpose is identification of the origin.""" def __init__(self): super().__init__(REMOTE_CONTROLLER_EVENT) class InputEventHandler: """A handler for an input event.""" def __init__( self, function: Callable[..., bool], description: str, learn_mode_enabled: bool = True, enabled: bool = True ) -> None: self.function: Callable[..., bool] = function self.description: str = description self.learn_mode_enabled: bool = learn_mode_enabled self._enabled: bool = enabled def __eq__(self, other: object) -> bool: if not isinstance(other, InputEventHandler): return False return self.function == other.function def __str__(self) -> str: return f"{self.description} (enabled: {self._enabled})" def is_enabled(self) -> bool: """Returns True if this handler is enabled.""" msg = f"INPUT EVENT HANDLER: {self.description} is enabled: {self._enabled}" debug.print_message(debug.LEVEL_INFO, msg, True) return self._enabled def set_enabled(self, enabled: bool) -> None: """Sets enabled state of this handler.""" self._enabled = enabled