From b24744b22beab2c22eda7e64126fea52b98801f6 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Fri, 16 Jan 2026 13:01:05 -0500 Subject: [PATCH] many many type hints added. Lots more to go. --- src/cthulhu/braille_generator.py | 1 + src/cthulhu/cthulhu.py | 220 ++-- src/cthulhu/cthulhu_state.py | 32 +- src/cthulhu/date_and_time_presenter.py | 24 +- src/cthulhu/debug.py | 90 +- src/cthulhu/event_manager.py | 115 +- src/cthulhu/flat_review_presenter.py | 21 +- src/cthulhu/focus_manager.py | 4 +- src/cthulhu/input_event.py | 39 +- src/cthulhu/notification_presenter.py | 25 +- src/cthulhu/plugin_system_manager.py | 124 +- src/cthulhu/script_manager.py | 82 +- src/cthulhu/script_utilities.py | 1033 ++++++++--------- .../scripts/terminal/script_utilities.py | 1 + src/cthulhu/signal_manager.py | 38 +- src/cthulhu/sound.py | 29 +- 16 files changed, 969 insertions(+), 909 deletions(-) diff --git a/src/cthulhu/braille_generator.py b/src/cthulhu/braille_generator.py index 33f3001..75a2aaf 100644 --- a/src/cthulhu/braille_generator.py +++ b/src/cthulhu/braille_generator.py @@ -36,6 +36,7 @@ gi.require_version("Atspi", "2.0") from gi.repository import Atspi from . import braille +from . import cthulhu # Need access to cthulhuApp from . import debug from . import generator from . import messages diff --git a/src/cthulhu/cthulhu.py b/src/cthulhu/cthulhu.py index 0727308..1dd740f 100644 --- a/src/cthulhu/cthulhu.py +++ b/src/cthulhu/cthulhu.py @@ -25,6 +25,8 @@ """The main module for the Cthulhu screen reader.""" +from __future__ import annotations + __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" @@ -34,22 +36,37 @@ __copyright__ = "Copyright (c) 2004-2009 Sun Microsystems Inc." \ __license__ = "LGPL" import faulthandler +from typing import TYPE_CHECKING, Any, Callable, Optional from . import dbus_service +if TYPE_CHECKING: + from types import FrameType + from gi.repository.Gio import Settings as GSettings + class APIHelper: """Helper class for plugin API interactions, including keybindings.""" - def __init__(self, app): + def __init__(self, app: Cthulhu) -> None: """Initialize the APIHelper. Arguments: - app: the Cthulhu application """ - self.app = app - self._gestureBindings = {} + self.app: Cthulhu = app + self._gestureBindings: dict[Optional[str], list[Any]] = {} - def registerGestureByString(self, function, name, gestureString, inputEventType='default', normalizer='cthulhu', learnModeEnabled=True, contextName=None, globalBinding=False): + def registerGestureByString( + self, + function: Callable[..., Any], + name: str, + gestureString: str, + inputEventType: str = 'default', + normalizer: str = 'cthulhu', + learnModeEnabled: bool = True, + contextName: Optional[str] = None, + globalBinding: bool = False + ) -> Optional[Any]: """Register a gesture by string.""" import logging logger = logging.getLogger(__name__) @@ -100,12 +117,12 @@ class APIHelper: # Create a keybinding handler class GestureHandler: - def __init__(self, function, description, learnModeEnabled=True): - self.function = function - self.description = description - self.learnModeEnabled = learnModeEnabled + def __init__(self, function: Callable[..., Any], description: str, learnModeEnabled: bool = True) -> None: + self.function: Callable[..., Any] = function + self.description: str = description + self.learnModeEnabled: bool = learnModeEnabled - def __call__(self, script, inputEvent): + def __call__(self, script: Any, inputEvent: Any) -> bool: try: logger.info(f"=== Plugin keybinding handler called! ===") return function(script, inputEvent) @@ -173,14 +190,14 @@ class APIHelper: return None - def unregisterShortcut(self, binding, contextName=None): + def unregisterShortcut(self, binding: Any, contextName: Optional[str] = None) -> None: """Unregister a previously registered shortcut. Arguments: - binding: the binding to unregister - contextName: the context for this gesture """ - removed_via_plugin_manager = False + removed_via_plugin_manager: bool = False if contextName and self.app: try: plugin_manager = self.app.getPluginSystemManager() @@ -221,7 +238,7 @@ from gi.repository import GObject try: from gi.repository.Gio import Settings - a11yAppSettings = Settings(schema_id='org.gnome.desktop.a11y.applications') + a11yAppSettings: Optional[GSettings] = Settings(schema_id='org.gnome.desktop.a11y.applications') except Exception: a11yAppSettings = None @@ -266,20 +283,20 @@ from . import resource_manager # Old global variables removed - now using cthulhuApp.* instead -def onEnabledChanged(gsetting, key): +def onEnabledChanged(gsetting: GSettings, key: str) -> None: try: - enabled = gsetting.get_boolean(key) + enabled: bool = gsetting.get_boolean(key) except Exception: return if key == 'screen-reader-enabled' and not enabled: shutdown() -EXIT_CODE_HANG = 50 +EXIT_CODE_HANG: int = 50 # The user-settings module (see loadUserSettings). # -_userSettings = None +_userSettings: Optional[Any] = None ######################################################################## # # @@ -287,29 +304,29 @@ _userSettings = None # # ######################################################################## -CARET_TRACKING = "caret-tracking" -FOCUS_TRACKING = "focus-tracking" -FLAT_REVIEW = "flat-review" -MOUSE_REVIEW = "mouse-review" -OBJECT_NAVIGATOR = "object-navigator" -SAY_ALL = "say-all" +CARET_TRACKING: str = "caret-tracking" +FOCUS_TRACKING: str = "focus-tracking" +FLAT_REVIEW: str = "flat-review" +MOUSE_REVIEW: str = "mouse-review" +OBJECT_NAVIGATOR: str = "object-navigator" +SAY_ALL: str = "say-all" -def getActiveModeAndObjectOfInterest(): +def getActiveModeAndObjectOfInterest() -> tuple[str, Any]: return focus_manager.get_manager().get_active_mode_and_object_of_interest() -def emitRegionChanged(obj, startOffset=None, endOffset=None, mode=None): +def emitRegionChanged(obj: Any, startOffset: Optional[int] = None, endOffset: Optional[int] = None, mode: Optional[str] = None) -> None: """Notifies interested clients that the current region of interest has changed.""" focus_manager.get_manager().emit_region_changed(obj, startOffset, endOffset, mode) -def setActiveWindow(frame, app=None, alsoSetLocusOfFocus=False, notifyScript=False): - real_app = app - real_frame = frame +def setActiveWindow(frame: Any, app: Optional[Any] = None, alsoSetLocusOfFocus: bool = False, notifyScript: bool = False) -> None: + real_app: Optional[Any] = app + real_frame: Any = frame if frame is not None and hasattr(AXObject, "find_real_app_and_window_for"): real_app, real_frame = AXObject.find_real_app_and_window_for(frame, app) focus_manager.get_manager().set_active_window( real_frame, real_app, set_window_as_focus=alsoSetLocusOfFocus, notify_script=notifyScript) -def setLocusOfFocus(event, obj, notifyScript=True, force=False): +def setLocusOfFocus(event: Optional[Any], obj: Any, notifyScript: bool = True, force: bool = False) -> None: """Sets the locus of focus (i.e., the object with visual focus) and notifies the script of the change should the script wish to present the change to the user. @@ -331,7 +348,7 @@ def setLocusOfFocus(event, obj, notifyScript=True, force=False): # # ######################################################################## -def _processBrailleEvent(event): +def _processBrailleEvent(event: Any) -> bool: """Called whenever a key is pressed on the Braille display. Arguments: @@ -340,7 +357,7 @@ def _processBrailleEvent(event): Returns True if the event was consumed; otherwise False """ - consumed = False + consumed: bool = False # Braille key presses always interrupt speech. # @@ -368,16 +385,16 @@ def _processBrailleEvent(event): # # ######################################################################## -def deviceChangeHandler(deviceManager, device): +def deviceChangeHandler(deviceManager: Any, device: Any) -> None: """New keyboards being plugged in stomp on our changes to the keymappings, so we have to re-apply""" source = device.get_source() if source == Gdk.InputSource.KEYBOARD: - msg = "CTHULHU: Keyboard change detected, re-creating the xmodmap" + msg: str = "CTHULHU: Keyboard change detected, re-creating the xmodmap" debug.printMessage(debug.LEVEL_INFO, msg, True) cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Keyboard change detected.") -def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): +def loadUserSettings(script: Optional[Any] = None, inputEvent: Optional[Any] = None, skipReloadMessage: bool = False) -> bool: """Loads (and reloads) the user settings module, reinitializing things such as speech if necessary. @@ -398,7 +415,7 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): cthulhuApp.scriptManager.deactivate() cthulhuApp.getSignalManager().emitSignal('load-setting-begin') - reloaded = False + reloaded: bool = False if _userSettings: _profile = cthulhuApp.settingsManager.getSetting('activeProfile')[1] try: @@ -491,7 +508,7 @@ def loadUserSettings(script=None, inputEvent=None, skipReloadMessage=False): return True -def _showPreferencesUI(script, prefs): +def _showPreferencesUI(script: Any, prefs: dict[str, Any]) -> None: if cthulhu_state.cthulhuOS: cthulhu_state.cthulhuOS.showGUI() return @@ -502,7 +519,7 @@ def _showPreferencesUI(script, prefs): debug.printException(debug.LEVEL_SEVERE) return - uiFile = os.path.join(cthulhu_platform.datadir, + uiFile: str = os.path.join(cthulhu_platform.datadir, cthulhu_platform.package, "ui", "cthulhu-setup.ui") @@ -511,7 +528,7 @@ def _showPreferencesUI(script, prefs): cthulhu_state.cthulhuOS.init(script) cthulhu_state.cthulhuOS.showGUI() -def showAppPreferencesGUI(script=None, inputEvent=None): +def showAppPreferencesGUI(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool: """Displays the user interface to configure the settings for a specific applications within Cthulhu and set up those app-specific user preferences using a GUI. @@ -519,7 +536,7 @@ def showAppPreferencesGUI(script=None, inputEvent=None): Returns True to indicate the input event has been consumed. """ - prefs = {} + prefs: dict[str, Any] = {} for key in settings.userCustomizableSettings: prefs[key] = cthulhuApp.settingsManager.getSetting(key) @@ -528,36 +545,36 @@ def showAppPreferencesGUI(script=None, inputEvent=None): return True -def showPreferencesGUI(script=None, inputEvent=None): +def showPreferencesGUI(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool: """Displays the user interface to configure Cthulhu and set up user preferences using a GUI. Returns True to indicate the input event has been consumed. """ - prefs = cthulhuApp.settingsManager.getGeneralSettings(cthulhuApp.settingsManager.profile) + prefs: dict[str, Any] = cthulhuApp.settingsManager.getGeneralSettings(cthulhuApp.settingsManager.profile) script = cthulhuApp.scriptManager.get_default_script() _showPreferencesUI(script, prefs) return True -def addKeyGrab(binding): +def addKeyGrab(binding: Any) -> list[int]: """ Add a key grab for the given key binding.""" manager = input_event_manager.get_manager() return manager.add_grabs_for_keybinding(binding) -def removeKeyGrab(id): +def removeKeyGrab(id: int) -> None: """ Remove the key grab for the given key binding.""" manager = input_event_manager.get_manager() manager.remove_grab_by_id(id) -def mapModifier(keycode): +def mapModifier(keycode: int) -> int: manager = input_event_manager.get_manager() return manager.map_keycode_to_modifier(keycode) -def quitCthulhu(script=None, inputEvent=None): +def quitCthulhu(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool: """Quit Cthulhu. Check if the user wants to confirm this action. If so, show the confirmation GUI otherwise just shutdown. @@ -568,7 +585,7 @@ def quitCthulhu(script=None, inputEvent=None): return True -def showFindGUI(script=None, inputEvent=None): +def showFindGUI(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> None: """Displays the user interface to perform an Cthulhu Find. Returns True to indicate the input event has been consumed. @@ -582,9 +599,9 @@ def showFindGUI(script=None, inputEvent=None): # If True, this module has been initialized. # -_initialized = False +_initialized: bool = False -def init(): +def init() -> bool: """Initialize the cthulhu module, which initializes the speech and braille modules. Also builds up the application list, registers for AT-SPI events, and creates scripts for all known applications. @@ -627,7 +644,7 @@ def init(): return True -def _start_dbus_service(): +def _start_dbus_service() -> bool: """Starts the D-Bus remote controller service in an idle callback.""" debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting D-Bus remote controller', True) try: @@ -652,11 +669,11 @@ def _start_dbus_service(): dbus_service.get_remote_controller().register_decorated_module("PluginSystemManager", plugin_manager) except Exception as e: - msg = f"CTHULHU: Failed to start D-Bus service: {e}" + msg: str = f"CTHULHU: Failed to start D-Bus service: {e}" debug.printMessage(debug.LEVEL_SEVERE, msg, True) return False # Remove the idle callback -def start(): +def start() -> None: """Starts Cthulhu.""" debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting', True) @@ -693,8 +710,8 @@ def start(): Atspi.event_main() -def die(exitCode=1): - pid = os.getpid() +def die(exitCode: int = 1) -> None: + pid: int = os.getpid() if exitCode == EXIT_CODE_HANG: # Someting is hung and we wish to abort. os.kill(pid, signal.SIGKILL) @@ -705,14 +722,14 @@ def die(exitCode=1): if exitCode > 1: os.kill(pid, signal.SIGTERM) -def timeout(signum=None, frame=None): - msg = 'TIMEOUT: something has hung. Aborting.' +def timeout(signum: Optional[int] = None, frame: Optional[FrameType] = None) -> None: + msg: str = 'TIMEOUT: something has hung. Aborting.' debug.printMessage(debug.LEVEL_SEVERE, msg, True) debug.printStack(debug.LEVEL_SEVERE) debug.examineProcesses(force=True) die(EXIT_CODE_HANG) -def shutdown(script=None, inputEvent=None): +def shutdown(script: Optional[Any] = None, inputEvent: Optional[Any] = None) -> bool: """Exits Cthulhu. Unregisters any event listeners and cleans up. Returns True if the shutdown procedure ran or False if this module @@ -769,12 +786,13 @@ def shutdown(script=None, inputEvent=None): return True -exitCount = 0 -def shutdownOnSignal(signum, frame): +exitCount: int = 0 + +def shutdownOnSignal(signum: int, frame: Optional[FrameType]) -> None: global exitCount - signalString = f'({signal.strsignal(signum)})' - msg = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}" + signalString: str = f'({signal.strsignal(signum)})' + msg: str = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}" debug.printMessage(debug.LEVEL_INFO, msg, True) # Well...we'll try to exit nicely, but if we keep getting called, @@ -800,7 +818,7 @@ def shutdownOnSignal(signum, frame): # speech.shutdown() shutdown() - cleanExit = True + cleanExit: bool = True except Exception: cleanExit = False @@ -810,28 +828,28 @@ def shutdownOnSignal(signum, frame): if not cleanExit: die(EXIT_CODE_HANG) -def crashOnSignal(signum, frame): - signalString = f'({signal.strsignal(signum)})' - msg = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}" +def crashOnSignal(signum: int, frame: Optional[FrameType]) -> None: + signalString: str = f'({signal.strsignal(signum)})' + msg: str = f"CTHULHU: Shutting down and exiting due to signal={signum} {signalString}" debug.printMessage(debug.LEVEL_SEVERE, msg, True) debug.printStack(debug.LEVEL_SEVERE) cthulhu_modifier_manager.getManager().unsetCthulhuModifiers("Crashed") sys.exit(1) -def main(): +def main() -> int: """The main entry point for Cthulhu. The exit codes for Cthulhu will loosely be based on signals, where the exit code will be the signal used to terminate Cthulhu (if a signal was used). Otherwise, an exit code of 0 means normal completion and an exit code of 50 means Cthulhu exited because of a hang.""" - msg = f"CTHULHU: Launching version {cthulhu_platform.version}" + msg: str = f"CTHULHU: Launching version {cthulhu_platform.version}" if cthulhu_platform.revision: msg += f" (rev {cthulhu_platform.revision})" - sessionType = os.environ.get('XDG_SESSION_TYPE') or "" - sessionDesktop = os.environ.get('XDG_SESSION_DESKTOP') or "" - session = "%s %s".strip() % (sessionType, sessionDesktop) + sessionType: str = os.environ.get('XDG_SESSION_TYPE') or "" + sessionDesktop: str = os.environ.get('XDG_SESSION_DESKTOP') or "" + session: str = "%s %s".strip() % (sessionType, sessionDesktop) if session: msg += f" session: {session}" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -896,7 +914,7 @@ def main(): class Cthulhu(GObject.Object): # basic signals - __gsignals__ = { + __gsignals__: dict[str, tuple[Any, ...]] = { "start-application-completed": (GObject.SignalFlags.RUN_LAST, None, ()), "stop-application-completed": (GObject.SignalFlags.RUN_LAST, None, ()), "load-setting-begin": (GObject.SignalFlags.RUN_LAST, None, ()), @@ -906,56 +924,56 @@ class Cthulhu(GObject.Object): "request-application-preferences": (GObject.SignalFlags.RUN_LAST, None, ()), "active-script-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), # New signal to indicate active script change } - def __init__(self): + def __init__(self) -> None: GObject.Object.__init__(self) # add members - self.resourceManager = resource_manager.ResourceManager(self) - self.settingsManager = settings_manager.SettingsManager(self) # Directly instantiate - self.eventManager = event_manager.EventManager(self) # Directly instantiate - self.scriptManager = script_manager.ScriptManager(self) # Directly instantiate - self.logger = logger.Logger() # Directly instantiate - self.signalManager = signal_manager.SignalManager(self) - self.dynamicApiManager = dynamic_api_manager.DynamicApiManager(self) - self.translationManager = translation_manager.TranslationManager(self) - self.debugManager = debug - self.APIHelper = APIHelper(self) + self.resourceManager: Any = resource_manager.ResourceManager(self) + self.settingsManager: Any = settings_manager.SettingsManager(self) # Directly instantiate + self.eventManager: Any = event_manager.EventManager(self) # Directly instantiate + self.scriptManager: Any = script_manager.ScriptManager(self) # Directly instantiate + self.logger: Any = logger.Logger() # Directly instantiate + self.signalManager: Any = signal_manager.SignalManager(self) + self.dynamicApiManager: Any = dynamic_api_manager.DynamicApiManager(self) + self.translationManager: Any = translation_manager.TranslationManager(self) + self.debugManager: Any = debug + self.APIHelper: APIHelper = APIHelper(self) self.createCompatAPI() - self.pluginSystemManager = plugin_system_manager.PluginSystemManager(self) + self.pluginSystemManager: Any = plugin_system_manager.PluginSystemManager(self) # Scan for available plugins at startup self.pluginSystemManager.rescanPlugins() - def getAPIHelper(self): + def getAPIHelper(self) -> APIHelper: return self.APIHelper - def getPluginSystemManager(self): + def getPluginSystemManager(self) -> Any: return self.pluginSystemManager - def getDynamicApiManager(self): + def getDynamicApiManager(self) -> Any: return self.dynamicApiManager - def getSignalManager(self): + def getSignalManager(self) -> Any: return self.signalManager - def getEventManager(self): + def getEventManager(self) -> Any: return self.eventManager - def getSettingsManager(self): + def getSettingsManager(self) -> Any: return self.settingsManager - def getScriptManager(self): + def getScriptManager(self) -> Any: return self.scriptManager - def get_scriptManager(self): + def get_scriptManager(self) -> Any: return self.scriptManager - def getDebugManager(self): + def getDebugManager(self) -> Any: return self.debugManager - def getTranslationManager(self): + def getTranslationManager(self) -> Any: return self.translationManager - def getResourceManager(self): + def getResourceManager(self) -> Any: return self.resourceManager - def getLogger(self): # New getter for the logger + def getLogger(self) -> Any: # New getter for the logger return self.logger - def addKeyGrab(self, binding): + def addKeyGrab(self, binding: Any) -> list[int]: return addKeyGrab(binding) - def removeKeyGrab(self, grab_id): + def removeKeyGrab(self, grab_id: int) -> None: return removeKeyGrab(grab_id) - def run(self, cacheValues=True): - return main(cacheValues) - def stop(self): + def run(self, cacheValues: bool = True) -> int: + return main() + def stop(self) -> None: pass - def createCompatAPI(self): + def createCompatAPI(self) -> None: # for now add compatibility layer using Dynamic API # should be removed step by step # use clean objects, getters and setters instead @@ -990,9 +1008,9 @@ class Cthulhu(GObject.Object): self.getDynamicApiManager().registerAPI('LoadUserSettings', loadUserSettings) self.getDynamicApiManager().registerAPI('APIHelper', self.APIHelper) -cthulhuApp = Cthulhu() +cthulhuApp: Cthulhu = Cthulhu() -def getManager(): +def getManager() -> Cthulhu: return cthulhuApp if __name__ == "__main__": diff --git a/src/cthulhu/cthulhu_state.py b/src/cthulhu/cthulhu_state.py index e2dee10..8cbb238 100644 --- a/src/cthulhu/cthulhu_state.py +++ b/src/cthulhu/cthulhu_state.py @@ -26,6 +26,8 @@ """Holds state that is shared among many modules. """ +from typing import Optional, Any + __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" @@ -36,54 +38,58 @@ __license__ = "LGPL" # NOTE: resist the temptation to do any imports here. They can # easily cause circular imports. # +# We use forward references in type hints to avoid importing: +# - Atspi.Accessible for focus and window +# - Script for activeScript +# - InputEvent for lastInputEvent # The Accessible that has visual focus. # -locusOfFocus = None +locusOfFocus: Optional[Any] = None # Actually: Optional[Atspi.Accessible] # The currently active window. # -activeWindow = None +activeWindow: Optional[Any] = None # Actually: Optional[Atspi.Accessible] # The currently active script. # -activeScript = None +activeScript: Optional[Any] = None # Actually: Optional[Script] # The currently active mode (focus, say all, flat review, etc.) and obj -activeMode = None -objOfInterest = None +activeMode: Optional[str] = None +objOfInterest: Optional[Any] = None # Actually: Optional[Atspi.Accessible] # Used to capture keys to redefine key bindings by the user. # -capturingKeys = False +capturingKeys: bool = False # The last non-modifier key event received. # -lastNonModifierKeyEvent = None +lastNonModifierKeyEvent: Optional[Any] = None # Actually: Optional[KeyboardEvent] # The InputEvent instance representing the last input event. This is # set each time a mouse, keyboard or braille event is received. # -lastInputEvent = None +lastInputEvent: Optional[Any] = None # Actually: Optional[InputEvent] # Used to determine if the user wishes Cthulhu to pass the next command # along to the current application rather than consuming it. # -bypassNextCommand = False +bypassNextCommand: bool = False # The last searchQuery # -searchQuery = None +searchQuery: Optional[str] = None # Handle to the Cthulhu Preferences Glade GUI object. # -cthulhuOS = None +cthulhuOS: Optional[Any] = None # Actually: Optional[CthulhuSetupGUI] # Set to True if the last key opened the preferences dialog # -openingDialog = False +openingDialog: bool = False # The AT-SPI device (needed for key grabs). Will be set to None if AT-SPI # is too old to support the new device API. # -device = None +device: Optional[Any] = None # Actually: Optional[Atspi.Device] diff --git a/src/cthulhu/date_and_time_presenter.py b/src/cthulhu/date_and_time_presenter.py index fd19c91..8371f1d 100644 --- a/src/cthulhu/date_and_time_presenter.py +++ b/src/cthulhu/date_and_time_presenter.py @@ -33,6 +33,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ __license__ = "LGPL" import time +from typing import Optional, Dict, Callable, Any from . import cthulhu # Need access to cthulhuApp from . import cmdnames @@ -45,21 +46,21 @@ _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class DateAndTimePresenter: """Provides commands to present the date and time.""" - def __init__(self): - self._handlers = self._setup_handlers() - self._bindings = self._setup_bindings() + def __init__(self) -> None: + self._handlers: Dict[str, input_event.InputEventHandler] = self._setup_handlers() + self._bindings: keybindings.KeyBindings = self._setup_bindings() - def get_bindings(self): + def get_bindings(self) -> keybindings.KeyBindings: """Returns the date-and-time-presenter keybindings.""" return self._bindings - def get_handlers(self): + def get_handlers(self) -> Dict[str, input_event.InputEventHandler]: """Returns the date-and-time-presenter handlers.""" return self._handlers - def _setup_handlers(self): + def _setup_handlers(self) -> Dict[str, input_event.InputEventHandler]: """Sets up and returns the date-and-time-presenter input event handlers.""" handlers = {} @@ -76,7 +77,7 @@ class DateAndTimePresenter: return handlers - def _setup_bindings(self): + def _setup_bindings(self) -> keybindings.KeyBindings: """Sets up and returns the date-and-time-presenter key bindings.""" bindings = keybindings.KeyBindings() @@ -99,14 +100,14 @@ class DateAndTimePresenter: return bindings - def present_time(self, script, event=None): + def present_time(self, script: Any, event: Optional[Any] = None) -> bool: """Presents the current time.""" format = cthulhu.cthulhuApp.settingsManager.getSetting('presentTimeFormat') script.presentMessage(time.strftime(format, time.localtime())) return True - def present_date(self, script, event=None): + def present_date(self, script: Any, event: Optional[Any] = None) -> bool: """Presents the current date.""" format = cthulhu.cthulhuApp.settingsManager.getSetting('presentDateFormat') @@ -114,8 +115,9 @@ class DateAndTimePresenter: return True -_presenter = None -def getPresenter(): +_presenter: Optional[DateAndTimePresenter] = None + +def getPresenter() -> DateAndTimePresenter: global _presenter if _presenter is None: _presenter = DateAndTimePresenter() diff --git a/src/cthulhu/debug.py b/src/cthulhu/debug.py index f1195b5..4c06563 100644 --- a/src/cthulhu/debug.py +++ b/src/cthulhu/debug.py @@ -28,6 +28,8 @@ level, which is held in the debugLevel field. All other methods take a debug level, which is compared to the current debug level to determine if the content should be output.""" +from __future__ import annotations + __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" @@ -41,16 +43,19 @@ import re import subprocess import sys import types - +from typing import Any, Optional, TextIO, Pattern, TYPE_CHECKING from datetime import datetime import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi -AXObject = None +if TYPE_CHECKING: + from .ax_object import AXObject as AXObjectType -def _get_ax_object(): +AXObject: Optional[type[AXObjectType]] = None + +def _get_ax_object() -> type[AXObjectType]: global AXObject if AXObject is None: from .ax_object import AXObject as ax_object @@ -117,14 +122,14 @@ LEVEL_FINEST = 400 # LEVEL_ALL = 0 -debugLevel = LEVEL_SEVERE +debugLevel: int = LEVEL_SEVERE # The debug file. If this is not set, then all debug output is done # via stdout. If this is set, then all debug output is sent to the # file. This can be useful for debugging because one can pass in a # non-buffered file to better track down hangs. # -debugFile = None +debugFile: Optional[TextIO] = None # The debug filter should be either None (which means to match all # events) or a compiled regular expression from the 're' module (see @@ -135,14 +140,14 @@ debugFile = None # # debug.eventDebugFilter = rc.compile('focus:|window:activate') # -eventDebugLevel = LEVEL_FINEST -eventDebugFilter = None +eventDebugLevel: int = LEVEL_FINEST +eventDebugFilter: Optional[Pattern[str]] = None # If True, we output debug information for the event queue. We # use this in addition to log level to prevent debug logic from # bogging down event handling. # -debugEventQueue = False +debugEventQueue: bool = False # What module(s) should be traced if traceit is being used. By default # we'll just attend to ourself. (And by default, we will not enable @@ -152,17 +157,17 @@ debugEventQueue = False # specific, immediate issue. Trust me. :-) Disabling braille monitor in # this case is also strongly advised. # -TRACE_MODULES = ['cthulhu'] +TRACE_MODULES: list[str] = ['cthulhu'] # Specific modules to ignore with traceit. # -TRACE_IGNORE_MODULES = ['traceback', 'linecache', 'locale', 'gettext', +TRACE_IGNORE_MODULES: list[str] = ['traceback', 'linecache', 'locale', 'gettext', 'logging', 'UserDict', 'encodings', 'posixpath', 'genericpath', 're'] # Specific apps to trace with traceit. # -TRACE_APPS = [] +TRACE_APPS: list[str] = [] # What AT-SPI event(s) should be traced if traceit is being used. By # default, we'll trace everything. Examples of what you might wish to @@ -173,7 +178,7 @@ TRACE_APPS = [] # TRACE_EVENTS = ['object:state-changed:selected'] # (if you know the exact event type of interest) # -TRACE_EVENTS = [] +TRACE_EVENTS: list[str] = [] # What role(s) should be traced if traceit is being used. By # default, we'll trace everything. An example of what you might wish @@ -181,17 +186,17 @@ TRACE_EVENTS = [] # # TRACE_ROLES = [Atspi.Role.PUSH_BUTTON, Atspi.Role.TOGGLE_BUTTON] # -TRACE_ROLES = [] +TRACE_ROLES: list[Atspi.Role] = [] # Whether or not traceit should only trace the work being done when # processing an actual event. This is when most bad things happen. # So we'll default to True. # -TRACE_ONLY_PROCESSING_EVENTS = True +TRACE_ONLY_PROCESSING_EVENTS: bool = True -objEvent = None +objEvent: Optional[Atspi.Event] = None -def printException(level): +def printException(level: int) -> None: """Prints out information regarding the current exception. Arguments: @@ -203,7 +208,7 @@ def printException(level): traceback.print_exc(100, debugFile) println(level) -def printStack(level): +def printStack(level: int) -> None: """Prints out the current stack. Arguments: @@ -215,7 +220,7 @@ def printStack(level): traceback.print_stack(None, 100, debugFile) println(level) -def _asString(obj): +def _asString(obj: Any) -> str: AXObject = _get_ax_object() if isinstance(obj, Atspi.Accessible): result = AXObject.get_role_name(obj) @@ -255,45 +260,47 @@ def _asString(obj): return str(obj) -def _format_tokens(tokens): +def _format_tokens(tokens: list[Any]) -> str: text = " ".join(map(_asString, tokens)) text = re.sub(r"[ \u00A0]+", " ", text) text = re.sub(r" (?=[,.:)])(?![\n])", "", text) return text -def format_log_message(prefix, message, reason=None): +def format_log_message(prefix: str, message: str, reason: Optional[str] = None) -> str: text = f"{prefix}: {message}" if reason: text = f"{text} (reason={reason})" return text -def print_log(level, prefix, message, reason=None, timestamp=False, stack=False): +def print_log(level: int, prefix: str, message: str, reason: Optional[str] = None, + timestamp: bool = False, stack: bool = False) -> None: text = format_log_message(prefix, message, reason) printMessage(level, text, timestamp, stack) -def print_log_tokens(level, prefix, tokens, reason=None, timestamp=False, stack=False): +def print_log_tokens(level: int, prefix: str, tokens: list[Any], reason: Optional[str] = None, + timestamp: bool = False, stack: bool = False) -> None: text = format_log_message(prefix, _format_tokens(tokens), reason) printMessage(level, text, timestamp, stack) -def printTokens(level, tokens, timestamp=False, stack=False): +def printTokens(level: int, tokens: list[Any], timestamp: bool = False, stack: bool = False) -> None: if level < debugLevel: return println(level, _format_tokens(tokens), timestamp, stack) -def print_tokens(level, tokens, timestamp=False, stack=False): +def print_tokens(level: int, tokens: list[Any], timestamp: bool = False, stack: bool = False) -> None: return printTokens(level, tokens, timestamp, stack) -def printMessage(level, text, timestamp=False, stack=False): +def printMessage(level: int, text: str, timestamp: bool = False, stack: bool = False) -> None: if level < debugLevel: return println(level, text, timestamp, stack) -def print_message(level, text, timestamp=False, stack=False): +def print_message(level: int, text: str, timestamp: bool = False, stack: bool = False) -> None: return printMessage(level, text, timestamp, stack) -def _stackAsString(max_frames=4): +def _stackAsString(max_frames: int = 4) -> str: callers = [] current_module = inspect.getmodule(inspect.currentframe()) stack = inspect.stack() @@ -313,7 +320,7 @@ def _stackAsString(max_frames=4): callers.reverse() return " > ".join(map(_asString, callers)) -def println(level, text="", timestamp=False, stack=False): +def println(level: int, text: str = "", timestamp: bool = False, stack: bool = False) -> None: """Prints the text to stderr unless debug is enabled. If debug is enabled the text will be redirected to the @@ -357,7 +364,7 @@ def println(level, text="", timestamp=False, stack=False): sys.stderr.writelines([text, "\n"]) sys.stderr.flush() -def printResult(level, result=None): +def printResult(level: int, result: Any = None) -> None: """Prints the return result, along with information about the method, arguments, and any errors encountered.""" @@ -380,7 +387,8 @@ def printResult(level, result=None): string = f'{callString}\nRESULT: {result}' println(level, f'{string}') -def printObjectEvent(level, event, sourceInfo=None, timestamp=False): +def printObjectEvent(level: int, event: Atspi.Event, sourceInfo: Optional[str] = None, + timestamp: bool = False) -> None: """Prints out an Python Event object. The given level may be overridden if the eventDebugLevel is greater. Furthermore, only events with event types matching the eventDebugFilter regular @@ -408,7 +416,7 @@ def printObjectEvent(level, event, sourceInfo=None, timestamp=False): if sourceInfo: println(level, f"{' ' * 18}{sourceInfo}", timestamp) -def printInputEvent(level, string, timestamp=False): +def printInputEvent(level: int, string: str, timestamp: bool = False) -> None: """Prints out an input event. The given level may be overridden if the eventDebugLevel (see setEventDebugLevel) is greater. @@ -419,7 +427,8 @@ def printInputEvent(level, string, timestamp=False): println(max(level, eventDebugLevel), string, timestamp) -def printDetails(level, indent, accessible, includeApp=True, timestamp=False): +def printDetails(level: int, indent: str, accessible: Atspi.Accessible, + includeApp: bool = True, timestamp: bool = False) -> None: """Lists the details of the given accessible with the given indentation. @@ -435,7 +444,8 @@ def printDetails(level, indent, accessible, includeApp=True, timestamp=False): getAccessibleDetails(level, accessible, indent, includeApp), timestamp) -def getAccessibleDetails(level, acc, indent="", includeApp=True): +def getAccessibleDetails(level: int, acc: Atspi.Accessible, indent: str = "", + includeApp: bool = True) -> str: """Returns a string, suitable for printing, that describes the given accessible. @@ -458,7 +468,7 @@ def getAccessibleDetails(level, acc, indent="", includeApp=True): # import linecache -def _getFileAndModule(frame): +def _getFileAndModule(frame: types.FrameType) -> tuple[Optional[str], Optional[str]]: filename, module = None, None try: filename = frame.f_globals["__file__"] @@ -471,7 +481,7 @@ def _getFileAndModule(frame): return filename, module -def _shouldTraceIt(): +def _shouldTraceIt() -> bool: AXObject = _get_ax_object() if not objEvent: return not TRACE_ONLY_PROCESSING_EVENTS @@ -491,7 +501,7 @@ def _shouldTraceIt(): return True -def traceit(frame, event, arg): +def traceit(frame: types.FrameType, event: str, arg: Any) -> Optional[object]: """Line tracing utility to output all lines as they are executed by the interpreter. This is to be used by sys.settrace and is for debugging purposes. @@ -543,14 +553,14 @@ def traceit(frame, event, arg): return traceit -def getOpenFDCount(pid): +def getOpenFDCount(pid: int) -> int: procs = subprocess.check_output([ 'lsof', '-w', '-Ff', '-p', str(pid)]) procs = procs.decode('UTF-8').split('\n') files = list(filter(lambda s: s and s[0] == 'f' and s[1:].isdigit(), procs)) return len(files) -def getCmdline(pid): +def getCmdline(pid: int) -> str: try: openFile = os.popen(f'cat /proc/{pid}/cmdline') cmdline = openFile.read() @@ -561,7 +571,7 @@ def getCmdline(pid): return cmdline -def pidOf(procName): +def pidOf(procName: str) -> list[int]: openFile = subprocess.Popen(f'pgrep {procName}', shell=True, stdout=subprocess.PIPE).stdout @@ -569,7 +579,7 @@ def pidOf(procName): openFile.close() return [int(p) for p in pids.split()] -def examineProcesses(force=False): +def examineProcesses(force: bool = False) -> None: AXObject = _get_ax_object() from .ax_utilities import AXUtilities if force: diff --git a/src/cthulhu/event_manager.py b/src/cthulhu/event_manager.py index 416d30e..827acc5 100644 --- a/src/cthulhu/event_manager.py +++ b/src/cthulhu/event_manager.py @@ -36,6 +36,7 @@ from gi.repository import GLib import queue import threading import time +from typing import Optional, Dict, List, Tuple, Any from . import cthulhu from . import debug @@ -49,25 +50,25 @@ from .ax_utilities import AXUtilities class EventManager: - EMBEDDED_OBJECT_CHARACTER = '\ufffc' + EMBEDDED_OBJECT_CHARACTER: str = '\ufffc' - def __init__(self, app, asyncMode=True): + def __init__(self, app: Any, asyncMode: bool = True) -> None: # app is CthulhuApp instance debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Initializing', True) debug.printMessage(debug.LEVEL_INFO, f'EVENT MANAGER: Async Mode is {asyncMode}', True) - self.app = app - self._asyncMode = asyncMode - self._scriptListenerCounts = {} - self._active = False - self._enqueueCount = 0 - self._dequeueCount = 0 - self._cmdlineCache = {} - self._eventQueue = queue.Queue(0) - self._gidleId = 0 - self._gidleLock = threading.Lock() - self._gilSleepTime = 0.00001 - self._synchronousToolkits = ['VCL'] - self._eventsSuspended = False - self._listener = Atspi.EventListener.new(self._enqueue) + self.app: Any = app + self._asyncMode: bool = asyncMode + self._scriptListenerCounts: Dict[str, int] = {} + self._active: bool = False + self._enqueueCount: int = 0 + self._dequeueCount: int = 0 + self._cmdlineCache: Dict[int, str] = {} + self._eventQueue: queue.Queue = queue.Queue(0) + self._gidleId: int = 0 + self._gidleLock: threading.Lock = threading.Lock() + self._gilSleepTime: float = 0.00001 + self._synchronousToolkits: List[str] = ['VCL'] + self._eventsSuspended: bool = False + self._listener: Atspi.EventListener = Atspi.EventListener.new(self._enqueue) # Note: These must match what the scripts registered for, otherwise # Atspi might segfault. @@ -75,24 +76,24 @@ class EventManager: # Events we don't want to suspend include: # object:text-changed:insert - marco # object:property-change:accessible-name - gnome-shell issue #6925 - self._suspendableEvents = ['object:children-changed:add', - 'object:children-changed:remove', - 'object:state-changed:sensitive', - 'object:state-changed:showing', - 'object:text-changed:delete'] - self._eventsTriggeringSuspension = [] - self._ignoredEvents = ['object:bounds-changed', - 'object:state-changed:defunct', - 'object:property-change:accessible-parent'] - self._parentsOfDefunctDescendants = [] + self._suspendableEvents: List[str] = ['object:children-changed:add', + 'object:children-changed:remove', + 'object:state-changed:sensitive', + 'object:state-changed:showing', + 'object:text-changed:delete'] + self._eventsTriggeringSuspension: List[Any] = [] # List of events + self._ignoredEvents: List[str] = ['object:bounds-changed', + 'object:state-changed:defunct', + 'object:property-change:accessible-parent'] + self._parentsOfDefunctDescendants: List[Any] = [] # List[Atspi.Accessible] cthulhu_state.device = None - self._keyHandlingActive = False - self._inputEventManager = None + self._keyHandlingActive: bool = False + self._inputEventManager: Optional[Any] = None # Optional[InputEventManager] debug.printMessage(debug.LEVEL_INFO, 'Event manager initialized', True) - def activate(self): + def activate(self) -> None: """Called when this event manager is activated.""" debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activating', True) @@ -101,7 +102,7 @@ class EventManager: GLib.idle_add(self._sync_focus_on_startup) debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Activated', True) - def _sync_focus_on_startup(self): + def _sync_focus_on_startup(self) -> bool: """Initialize active window and focus when startup missed focus events.""" focus = cthulhu_state.locusOfFocus @@ -127,7 +128,7 @@ class EventManager: return False - def _activateKeyHandling(self): + def _activateKeyHandling(self) -> None: """Activates keyboard handling using InputEventManager with Atspi.Device.""" if self._keyHandlingActive: @@ -140,7 +141,7 @@ class EventManager: self._keyHandlingActive = True debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Keyboard handling activated', True) - def _deactivateKeyHandling(self): + def _deactivateKeyHandling(self) -> None: """Deactivates keyboard handling.""" if not self._keyHandlingActive: @@ -153,7 +154,7 @@ class EventManager: self._keyHandlingActive = False debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Keyboard handling deactivated', True) - def deactivate(self): + def deactivate(self) -> None: """Called when this event manager is deactivated.""" debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Deactivating', True) @@ -163,23 +164,23 @@ class EventManager: self._deactivateKeyHandling() debug.printMessage(debug.LEVEL_INFO, 'EVENT MANAGER: Deactivated', True) - def ignoreEventTypes(self, eventTypeList): + def ignoreEventTypes(self, eventTypeList: List[str]) -> None: for eventType in eventTypeList: if eventType not in self._ignoredEvents: self._ignoredEvents.append(eventType) - def unignoreEventTypes(self, eventTypeList): + def unignoreEventTypes(self, eventTypeList: List[str]) -> None: for eventType in eventTypeList: if eventType in self._ignoredEvents: self._ignoredEvents.remove(eventType) - def _isDuplicateEvent(self, event): + def _isDuplicateEvent(self, event: Any) -> bool: # event: Atspi.Event """Returns True if this event is already in the event queue.""" if self._inFlood() and self._prioritizeDuringFlood(event): return False - def isSame(x): + def isSame(x: Any) -> bool: return x.type == event.type \ and x.source == event.source \ and x.detail1 == event.detail1 \ @@ -192,7 +193,7 @@ class EventManager: return False - def _getAppCmdline(self, app): + def _getAppCmdline(self, app: Any) -> str: # app: Atspi.Accessible pid = AXObject.get_process_id(app) if pid == -1: return "" @@ -203,7 +204,7 @@ class EventManager: self._cmdlineCache[pid] = cmdline return cmdline - def _isSteamApp(self, app): + def _isSteamApp(self, app: Any) -> bool: # app: Atspi.Accessible name = AXObject.get_name(app) if not name: nameLower = "" @@ -216,7 +217,7 @@ class EventManager: cmdline = self._getAppCmdline(app) return "steamwebhelper" in cmdline - def _isSteamNotificationEvent(self, event): + def _isSteamNotificationEvent(self, event: Any) -> bool: # event: Atspi.Event for obj in (event.any_data, event.source): if not isinstance(obj, Atspi.Accessible): continue @@ -229,7 +230,7 @@ class EventManager: return False - def _ignore(self, event): + def _ignore(self, event: Any) -> bool: # event: Atspi.Event """Returns True if this event should be ignored.""" app = AXObject.get_application(event.source) @@ -701,7 +702,7 @@ class EventManager: return rerun - def registerListener(self, eventType): + def registerListener(self, eventType: str) -> None: """Tells this module to listen for the given event type. Arguments: @@ -717,7 +718,7 @@ class EventManager: self._listener.register(eventType) self._scriptListenerCounts[eventType] = 1 - def deregisterListener(self, eventType): + def deregisterListener(self, eventType: str) -> None: """Tells this module to stop listening for the given event type. Arguments: @@ -735,7 +736,7 @@ class EventManager: self._listener.deregister(eventType) del self._scriptListenerCounts[eventType] - def registerScriptListeners(self, script): + def registerScriptListeners(self, script: Any) -> None: # script: Script """Tells the event manager to start listening for all the event types of interest to the script. @@ -749,7 +750,7 @@ class EventManager: for eventType in script.listeners.keys(): self.registerListener(eventType) - def deregisterScriptListeners(self, script): + def deregisterScriptListeners(self, script: Any) -> None: # script: Script """Tells the event manager to stop listening for all the event types of interest to the script. @@ -797,7 +798,7 @@ class EventManager: debug.printMessage(debug.eventDebugLevel, msg, False) @staticmethod - def _get_scriptForEvent(event): + def _get_scriptForEvent(event: Any) -> Optional[Any]: # Returns Optional[Script] """Returns the script associated with event.""" if event.type.startswith("mouse:"): @@ -835,7 +836,7 @@ class EventManager: debug.printTokens(debug.LEVEL_INFO, tokens, True) return script - def _isActivatableEvent(self, event, script=None): + def _isActivatableEvent(self, event: Any, script: Optional[Any] = None) -> Tuple[bool, str]: """Determines if the event is one which should cause us to change which script is currently active. @@ -901,7 +902,7 @@ class EventManager: return False, "No reason found to activate a different script." - def _eventSourceIsDead(self, event): + def _eventSourceIsDead(self, event: Any) -> bool: # event: Atspi.Event if AXObject.is_dead(event.source): tokens = ["EVENT MANAGER: source of", event.type, "is dead"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -909,7 +910,7 @@ class EventManager: return False - def _ignoreDuringDeluge(self, event): + def _ignoreDuringDeluge(self, event: Any) -> bool: # event: Atspi.Event """Returns true if this event should be ignored during a deluge.""" if self._eventSourceIsDead(event): @@ -936,7 +937,7 @@ class EventManager: return event.source != cthulhu_state.locusOfFocus - def _inDeluge(self): + def _inDeluge(self) -> bool: size = self._eventQueue.qsize() if size > 100: msg = f"EVENT MANAGER: DELUGE! Queue size is {size}" @@ -945,7 +946,7 @@ class EventManager: return False - def _processDuringFlood(self, event): + def _processDuringFlood(self, event: Any) -> bool: # event: Atspi.Event """Returns true if this event should be processed during a flood.""" if self._eventSourceIsDead(event): @@ -972,7 +973,7 @@ class EventManager: return event.source == cthulhu_state.locusOfFocus - def _prioritizeDuringFlood(self, event): + def _prioritizeDuringFlood(self, event: Any) -> bool: # event: Atspi.Event """Returns true if this event should be prioritized during a flood.""" if event.type.startswith("object:state-changed:focused"): @@ -1001,7 +1002,7 @@ class EventManager: return False - def _pruneEventsDuringFlood(self): + def _pruneEventsDuringFlood(self) -> None: """Gets rid of events we don't care about during a flood.""" oldSize = self._eventQueue.qsize() @@ -1024,7 +1025,7 @@ class EventManager: msg = f"EVENT MANAGER: {oldSize - newSize} events pruned. New size: {newSize}" debug.printMessage(debug.LEVEL_INFO, msg, True) - def _inFlood(self): + def _inFlood(self) -> bool: size = self._eventQueue.qsize() if size > 50: msg = f"EVENT MANAGER: FLOOD? Queue size is {size}" @@ -1132,7 +1133,7 @@ class EventManager: msg = f"EVENT MANAGER: {key}: {value}" debug.printMessage(debug.LEVEL_INFO, msg, True) - def processBrailleEvent(self, brailleEvent): + def processBrailleEvent(self, brailleEvent: Any) -> bool: # brailleEvent: BrailleEvent """Called whenever a cursor key is pressed on the Braille display. Arguments: @@ -1148,9 +1149,9 @@ class EventManager: else: return False -_manager = None +_manager: Optional[EventManager] = None -def getManager(): +def getManager() -> EventManager: global _manager if _manager is None: _manager = cthulhu.cthulhuApp.eventManager diff --git a/src/cthulhu/flat_review_presenter.py b/src/cthulhu/flat_review_presenter.py index ebcd238..bb0cc51 100644 --- a/src/cthulhu/flat_review_presenter.py +++ b/src/cthulhu/flat_review_presenter.py @@ -33,6 +33,7 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc." \ __license__ = "LGPL" import gi +from typing import Optional, Dict, Callable, Any gi.require_version("Gtk", "3.0") from gi.repository import Gtk @@ -55,21 +56,21 @@ _settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager class FlatReviewPresenter: """Provides access to on-screen objects via flat-review.""" - def __init__(self): - self._context = None - self._current_contents = "" - self._restrict = cthulhu.cthulhuApp.settingsManager.getSetting("flatReviewIsRestricted") - self._handlers = self._setup_handlers() - self._desktop_bindings = self._setup_desktop_bindings() - self._laptop_bindings = self._setup_laptop_bindings() - self._gui = None + def __init__(self) -> None: + self._context: Optional[flat_review.Context] = None + self._current_contents: str = "" + self._restrict: bool = cthulhu.cthulhuApp.settingsManager.getSetting("flatReviewIsRestricted") + self._handlers: Dict[str, Callable] = self._setup_handlers() + self._desktop_bindings: keybindings.KeyBindings = self._setup_desktop_bindings() + self._laptop_bindings: keybindings.KeyBindings = self._setup_laptop_bindings() + self._gui: Optional[Any] = None # Optional[Gtk.Window] - def is_active(self): + def is_active(self) -> bool: """Returns True if the flat review presenter is active.""" return self._context is not None - def get_or_create_context(self, script=None): + def get_or_create_context(self, script: Optional[Any] = None) -> Optional[flat_review.Context]: """Returns the flat review context, creating one if necessary.""" # TODO - JD: Scripts should not be able to interact with the diff --git a/src/cthulhu/focus_manager.py b/src/cthulhu/focus_manager.py index fbf3df0..2068a2f 100644 --- a/src/cthulhu/focus_manager.py +++ b/src/cthulhu/focus_manager.py @@ -59,10 +59,10 @@ def _get_ax_utilities(): from .ax_utilities import AXUtilities return AXUtilities -def _log(message, reason=None, timestamp=True, stack=False): +def _log(message: str, reason: Optional[str] = None, timestamp: bool = True, stack: bool = False) -> None: debug.print_log(debug.LEVEL_INFO, "FOCUS MANAGER", message, reason, timestamp, stack) -def _log_tokens(tokens, reason=None, timestamp=True, stack=False): +def _log_tokens(tokens: list, reason: Optional[str] = None, timestamp: bool = True, stack: bool = False) -> None: debug.print_log_tokens(debug.LEVEL_INFO, "FOCUS MANAGER", tokens, reason, timestamp, stack) if TYPE_CHECKING: diff --git a/src/cthulhu/input_event.py b/src/cthulhu/input_event.py index b3a753e..476aa52 100644 --- a/src/cthulhu/input_event.py +++ b/src/cthulhu/input_event.py @@ -39,6 +39,7 @@ from gi.repository import Atspi import math import time +from typing import Optional, Any from gi.repository import Gdk from gi.repository import GLib @@ -54,26 +55,26 @@ from . import settings from .ax_object import AXObject from .ax_utilities import AXUtilities -KEYBOARD_EVENT = "keyboard" -BRAILLE_EVENT = "braille" -MOUSE_BUTTON_EVENT = "mouse:button" -REMOTE_CONTROLLER_EVENT = "remote controller" +KEYBOARD_EVENT: str = "keyboard" +BRAILLE_EVENT: str = "braille" +MOUSE_BUTTON_EVENT: str = "mouse:button" +REMOTE_CONTROLLER_EVENT: str = "remote controller" class InputEvent: - def __init__(self, eventType): + def __init__(self, eventType: str) -> None: """Creates a new KEYBOARD_EVENT, BRAILLE_EVENT, or MOUSE_BUTTON_EVENT.""" - self.type = eventType - self.time = time.time() - self._clickCount = 0 + self.type: str = eventType + self.time: float = time.time() + self._clickCount: int = 0 - def get_click_count(self): + def get_click_count(self) -> int: """Return the count of the number of clicks a user has made.""" return self._clickCount - def set_click_count(self, count=None): + def set_click_count(self, count: Optional[int] = None) -> None: """Updates the count of the number of clicks a user has made.""" if count is None: @@ -82,21 +83,21 @@ class InputEvent: self._clickCount = count class KeyboardEvent(InputEvent): - stickyKeys = False + stickyKeys: bool = False - duplicateCount = 0 - cthulhuModifierPressed = False + duplicateCount: int = 0 + cthulhuModifierPressed: bool = False # Whether last press of the Cthulhu modifier was alone - lastCthulhuModifierAlone = False - lastCthulhuModifierAloneTime = None + lastCthulhuModifierAlone: bool = False + lastCthulhuModifierAloneTime: Optional[float] = None # Whether the current press of the Cthulhu modifier is alone - currentCthulhuModifierAlone = False - currentCthulhuModifierAloneTime = None + currentCthulhuModifierAlone: bool = False + currentCthulhuModifierAloneTime: Optional[float] = None # When the second cthulhu press happened - secondCthulhuModifierTime = None + secondCthulhuModifierTime: Optional[float] = None # Sticky modifiers state, to be applied to the next keyboard event - cthulhuStickyModifiers = 0 + cthulhuStickyModifiers: int = 0 TYPE_UNKNOWN = "unknown" TYPE_PRINTABLE = "printable" diff --git a/src/cthulhu/notification_presenter.py b/src/cthulhu/notification_presenter.py index 2e8e22f..0228511 100644 --- a/src/cthulhu/notification_presenter.py +++ b/src/cthulhu/notification_presenter.py @@ -33,6 +33,7 @@ __copyright__ = "Copyright (c) 2023 Igalia, S.L." \ __license__ = "LGPL" import time +from typing import Optional, Dict, List, Any, Callable import gi gi.require_version('Gtk', '3.0') @@ -52,30 +53,30 @@ from . import cthulhu_state class NotificationPresenter: """Provides access to the notification history.""" - def __init__(self): - self._gui = None - self._handlers = self._setup_handlers() - self._bindings = self._setup_bindings() - self._max_size = 55 + def __init__(self) -> None: + self._gui: Optional[Any] = None # Optional[Gtk.Window] + self._handlers: Dict[str, Callable] = self._setup_handlers() + self._bindings: keybindings.KeyBindings = self._setup_bindings() + self._max_size: int = 55 # The list is arranged with the most recent message being at the end of # the list. The current index is relative to, and used directly, with the # python list, i.e. self._notifications[-3] would return the third-to-last # notification message. - self._notifications = [] - self._current_index = -1 + self._notifications: List[List[Any]] = [] # List of [message: str, time: float] + self._current_index: int = -1 - def get_bindings(self): + def get_bindings(self) -> keybindings.KeyBindings: """Returns the notification-presenter keybindings.""" return self._bindings - def get_handlers(self): + def get_handlers(self) -> Dict[str, Callable]: """Returns the notification-presenter handlers.""" return self._handlers - def save_notification(self, message): + def save_notification(self, message: str) -> None: """Adds message to the list of notification messages.""" tokens = ["NOTIFICATION PRESENTER: Adding '", message, "'."] @@ -84,7 +85,7 @@ class NotificationPresenter: self._notifications = self._notifications[to_remove:] self._notifications.append([message, time.time()]) - def clear_list(self): + def clear_list(self) -> None: """Clears the notifications list.""" msg = "NOTIFICATION PRESENTER: Clearing list." @@ -92,7 +93,7 @@ class NotificationPresenter: self._notifications = [] self._current_index = -1 - def _setup_handlers(self): + def _setup_handlers(self) -> Dict[str, Callable]: """Sets up and returns the notification-presenter input event handlers.""" handlers = {} diff --git a/src/cthulhu/plugin_system_manager.py b/src/cthulhu/plugin_system_manager.py index 03f46f0..7c314cd 100644 --- a/src/cthulhu/plugin_system_manager.py +++ b/src/cthulhu/plugin_system_manager.py @@ -18,6 +18,7 @@ import re import shutil import subprocess from enum import IntEnum +from typing import Optional, Dict, List, Any import pluggy from . import dbus_service @@ -25,23 +26,23 @@ from . import input_event_manager from . import keybindings # Added import # Set to True for more detailed plugin loading debug info -PLUGIN_DEBUG = True +PLUGIN_DEBUG: bool = True -logger = logging.getLogger(__name__) +logger: logging.Logger = logging.getLogger(__name__) if PLUGIN_DEBUG: logger.setLevel(logging.DEBUG) -LEGACY_PLUGIN_NAME_ALIASES = { +LEGACY_PLUGIN_NAME_ALIASES: Dict[str, str] = { "ocrdesktop": "OCR", } -LEGACY_PLUGIN_DIR_ALIASES = { +LEGACY_PLUGIN_DIR_ALIASES: Dict[str, str] = { "OCRDesktop": "OCR", } -_manager = None +_manager: Optional['PluginSystemManager'] = None -def getManager(): +def getManager() -> Optional['PluginSystemManager']: """Return the shared PluginSystemManager instance.""" return _manager @@ -50,7 +51,7 @@ class PluginType(IntEnum): SYSTEM = 1 USER = 2 - def get_root_dir(self): + def get_root_dir(self) -> str: """Returns the directory where this type of plugins can be found.""" if self.value == PluginType.SYSTEM: current_file = inspect.getfile(inspect.currentframe()) @@ -63,75 +64,84 @@ class PluginType(IntEnum): class PluginInfo: """Information about a plugin.""" - def __init__(self, name, module_name, module_dir, metadata=None, canonical_name=None, source_id=None, origin=None): - self.name = name - self.module_name = module_name - self.module_dir = module_dir - self.metadata = metadata or {} - self.canonical_name = canonical_name or module_name - self.source_id = source_id or "unknown" - self.origin = origin or "unknown" - self.preferred_alias = False - self.builtin = False - self.hidden = False - self.module = None - self.instance = None - self.loaded = False + def __init__( + self, + name: str, + module_name: str, + module_dir: str, + metadata: Optional[Dict[str, Any]] = None, + canonical_name: Optional[str] = None, + source_id: Optional[str] = None, + origin: Optional[str] = None + ) -> None: + self.name: str = name + self.module_name: str = module_name + self.module_dir: str = module_dir + self.metadata: Dict[str, Any] = metadata or {} + self.canonical_name: str = canonical_name or module_name + self.source_id: str = source_id or "unknown" + self.origin: str = origin or "unknown" + self.preferred_alias: bool = False + self.builtin: bool = False + self.hidden: bool = False + self.module: Optional[Any] = None + self.instance: Optional[Any] = None + self.loaded: bool = False - def get_module_name(self): + def get_module_name(self) -> str: return self.module_name - def get_canonical_name(self): + def get_canonical_name(self) -> str: return self.canonical_name - def get_name(self): + def get_name(self) -> str: return self.metadata.get('name', self.name) - def get_version(self): + def get_version(self) -> str: return self.metadata.get('version', '0.0.0') - def get_description(self): + def get_description(self) -> str: return self.metadata.get('description', '') - def get_source_id(self): + def get_source_id(self) -> str: return self.source_id - def get_origin(self): + def get_origin(self) -> str: return self.origin - def get_source_label(self): + def get_source_label(self) -> str: if self.origin == "sources": return self.source_id return self.origin - def get_module_dir(self): + def get_module_dir(self) -> str: return self.module_dir class PluginSystemManager: """Cthulhu Plugin Manager using pluggy.""" - def __init__(self, app): + def __init__(self, app: Any) -> None: # app is CthulhuApp instance global _manager - self.app = app + self.app: Any = app logger.info("Initializing PluginSystemManager") _manager = self # Initialize plugin manager logger.info("Setting up plugin manager") - self.plugin_manager = pluggy.PluginManager("cthulhu") + self.plugin_manager: pluggy.PluginManager = pluggy.PluginManager("cthulhu") # Define hook specifications hook_spec = pluggy.HookspecMarker("cthulhu") class CthulhuHookSpecs: @hook_spec - def activate(self, plugin=None): + def activate(self, plugin: Optional[Any] = None) -> None: """Called when the plugin is activated.""" pass @hook_spec - def deactivate(self, plugin=None): + def deactivate(self, plugin: Optional[Any] = None) -> None: """Called when the plugin is deactivated.""" pass @@ -139,13 +149,13 @@ class PluginSystemManager: self.plugin_manager.add_hookspecs(CthulhuHookSpecs) # Plugin storage - self._plugins = {} # module_name -> PluginInfo - self._plugin_name_index = {} # canonical_name -> [module_name] - self._active_plugins = [] - self._plugin_keybindings = {} # plugin_name -> [KeyBinding] - self._global_keybindings = keybindings.KeyBindings() - self._global_bindings = [] - self._last_active_script = None + self._plugins: Dict[str, PluginInfo] = {} # module_name -> PluginInfo + self._plugin_name_index: Dict[str, List[str]] = {} # canonical_name -> [module_name] + self._active_plugins: List[str] = [] + self._plugin_keybindings: Dict[str, List[Any]] = {} # plugin_name -> [KeyBinding] + self._global_keybindings: keybindings.KeyBindings = keybindings.KeyBindings() + self._global_bindings: List[Any] = [] # List[KeyBinding] + self._last_active_script: Optional[Any] = None # Optional[Script] # Create plugin directories self._setup_plugin_dirs() @@ -153,12 +163,12 @@ class PluginSystemManager: # Log available plugins directory paths logger.info(f"System plugins directory: {PluginType.SYSTEM.get_root_dir()}") logger.info(f"User plugins directory: {PluginType.USER.get_root_dir()}") - + # Connect to active-script-changed signal self.app.getSignalManager().connectSignal( 'active-script-changed', self._on_active_script_changed, 'default') - def add_keybinding(self, plugin_name, binding, global_binding=False): + def add_keybinding(self, plugin_name: str, binding: Any, global_binding: bool = False) -> None: # binding: KeyBinding """Add a keybinding associated with a specific plugin.""" if plugin_name not in self._plugin_keybindings: self._plugin_keybindings[plugin_name] = [] @@ -174,14 +184,14 @@ class PluginSystemManager: else: logger.warning(f"Failed to create global key grab for {binding.keysymstring}") - def activate_keybindings_for_plugin(self, plugin_name): + def activate_keybindings_for_plugin(self, plugin_name: str) -> None: """Activates keybindings for a single plugin with the active script.""" plugin_info = self._plugins.get(plugin_name) if not plugin_info or not plugin_info.loaded: return self._activate_plugin_keybindings(plugin_info) - def remove_keybinding(self, plugin_name, binding): + def remove_keybinding(self, plugin_name: str, binding: Any) -> None: # binding: KeyBinding """Remove a keybinding associated with a specific plugin.""" if plugin_name in self._plugin_keybindings: if binding in self._plugin_keybindings[plugin_name]: @@ -204,7 +214,7 @@ class PluginSystemManager: active_script.getKeyBindings().remove(binding) input_event_manager.get_manager().remove_grabs_for_keybinding(binding) - def _activate_plugin_keybindings(self, plugin_info): + def _activate_plugin_keybindings(self, plugin_info: PluginInfo) -> None: """Activates all keybindings for a given plugin with the active script.""" from . import cthulhu_state # Import here to avoid circular dependency if not cthulhu_state.activeScript: @@ -227,7 +237,7 @@ class PluginSystemManager: logger.warning(f"Failed to create key grab for {binding.keysymstring} for plugin {plugin_name}") logger.debug(f"Activated keybinding '{binding.asString()}' for plugin '{plugin_name}'") - def _deactivate_plugin_keybindings(self, plugin_info): + def _deactivate_plugin_keybindings(self, plugin_info: PluginInfo) -> None: """Deactivates all keybindings for a given plugin from the active script.""" from . import cthulhu_state # Import here to avoid circular dependency if not cthulhu_state.activeScript: @@ -252,16 +262,16 @@ class PluginSystemManager: input_manager.remove_grabs_for_keybinding(binding) logger.debug(f"Deactivated keybinding '{binding.asString()}' for plugin '{plugin_name}'") - def refresh_active_script_keybindings(self): + def refresh_active_script_keybindings(self) -> None: """Public method to refresh keybindings for the currently active script.""" from . import cthulhu_state if cthulhu_state.activeScript: self._on_active_script_changed(self.app, cthulhu_state.activeScript) - def get_global_keybindings(self): + def get_global_keybindings(self) -> keybindings.KeyBindings: return self._global_keybindings - def _on_active_script_changed(self, app, new_script): + def _on_active_script_changed(self, app: Any, new_script: Optional[Any]) -> None: """Called when the active script changes. Re-applies keybindings for all active plugins.""" logger.info(f"Active script changed to {new_script.name if new_script else 'None'}. Re-applying plugin keybindings.") @@ -287,28 +297,28 @@ class PluginSystemManager: if plugin_info and plugin_info.loaded: self._activate_plugin_keybindings(plugin_info) - def _setup_plugin_dirs(self): + def _setup_plugin_dirs(self) -> None: """Ensure plugin directories exist.""" os.makedirs(PluginType.SYSTEM.get_root_dir(), exist_ok=True) os.makedirs(PluginType.USER.get_root_dir(), exist_ok=True) os.makedirs(self._get_plugin_sources_root(), exist_ok=True) - def _get_plugin_sources_root(self): + def _get_plugin_sources_root(self) -> str: return os.path.expanduser('~/.local/share/cthulhu/plugin-sources') - def _get_additional_plugin_dirs(self): + def _get_additional_plugin_dirs(self) -> List[str]: return [os.path.expanduser('~/.local/share/plugins')] - def _path_under_root(self, path, root): + def _path_under_root(self, path: str, root: str) -> bool: try: return os.path.commonpath([os.path.abspath(path), os.path.abspath(root)]) == os.path.abspath(root) except ValueError: return False - def _sanitize_source_id(self, source_id): + def _sanitize_source_id(self, source_id: str) -> str: return re.sub(r'[^a-zA-Z0-9._-]+', '-', source_id).strip('-') or "source" - def _get_origin_info(self, plugin_dir): + def _get_origin_info(self, plugin_dir: str) -> tuple[str, str]: system_root = PluginType.SYSTEM.get_root_dir() user_root = PluginType.USER.get_root_dir() local_root = os.path.expanduser('~/.local/share/plugins') diff --git a/src/cthulhu/script_manager.py b/src/cthulhu/script_manager.py index bf36bbf..20ac2e4 100644 --- a/src/cthulhu/script_manager.py +++ b/src/cthulhu/script_manager.py @@ -30,41 +30,46 @@ __copyright__ = "Copyright (c) 2011. Cthulhu Team." __license__ = "LGPL" import importlib +from typing import Optional, Dict, Any from . import debug from . import cthulhu_state from .ax_object import AXObject from .scripts import apps, toolkits +# Forward references to avoid circular imports +# Script is defined in script.py +# Atspi.Accessible comes from AT-SPI + def _get_ax_utilities(): # Avoid circular import with ax_utilities -> ax_utilities_event -> focus_manager -> braille -> settings_manager -> script_manager. from .ax_utilities import AXUtilities return AXUtilities -def _log(message, reason=None, timestamp=True, stack=False): +def _log(message: str, reason: Optional[str] = None, timestamp: bool = True, stack: bool = False) -> None: debug.print_log(debug.LEVEL_INFO, "SCRIPT MANAGER", message, reason, timestamp, stack) -def _log_tokens(tokens, reason=None, timestamp=True, stack=False): +def _log_tokens(tokens: list, reason: Optional[str] = None, timestamp: bool = True, stack: bool = False) -> None: debug.print_log_tokens(debug.LEVEL_INFO, "SCRIPT MANAGER", tokens, reason, timestamp, stack) class ScriptManager: - def __init__(self, app): # Added app argument + def __init__(self, app: Any) -> None: # app is the CthulhuApp instance _log("Initializing") - self.app = app # Store app instance - self.appScripts = {} - self.toolkitScripts = {} - self.customScripts = {} - self._sleepModeScripts = {} - self._appModules = apps.__all__ - self._toolkitModules = toolkits.__all__ - self._defaultScript = None - self._scriptPackages = \ + self.app: Any = app # Store app instance + self.appScripts: Dict[Any, Any] = {} # Dict[Atspi.Accessible, Script] + self.toolkitScripts: Dict[Any, Dict[str, Any]] = {} # Dict[Atspi.Accessible, Dict[str, Script]] + self.customScripts: Dict[Any, Dict[str, Any]] = {} # Dict[Atspi.Accessible, Dict[str, Script]] + self._sleepModeScripts: Dict[Any, Any] = {} # Dict[Atspi.Accessible, Script] + self._appModules: list = apps.__all__ + self._toolkitModules: list = toolkits.__all__ + self._defaultScript: Optional[Any] = None # Optional[Script] + self._scriptPackages: list[str] = \ ["cthulhu-scripts", "cthulhu.scripts", "cthulhu.scripts.apps", "cthulhu.scripts.toolkits"] - self._appNames = \ + self._appNames: Dict[str, str] = \ {'Icedove': 'Thunderbird', 'Nereid': 'Banshee', 'gnome-calculator': 'gcalctool', @@ -75,14 +80,14 @@ class ScriptManager: 'metacity': 'switcher', 'pluma': 'gedit', } - self._toolkitNames = \ + self._toolkitNames: Dict[str, str] = \ {'WebKitGTK': 'WebKitGtk', 'GTK': 'gtk'} self.set_active_script(None, "lifecycle: init") - self._active = False + self._active: bool = False _log("Initialized") - def activate(self): + def activate(self) -> None: """Called when this script manager is activated.""" _log("Activating") @@ -92,7 +97,7 @@ class ScriptManager: self._active = True _log("Activated") - def deactivate(self): + def deactivate(self) -> None: """Called when this script manager is deactivated.""" _log("Deactivating") @@ -106,7 +111,7 @@ class ScriptManager: self._active = False _log("Deactivated") - def get_module_name(self, app): + def get_module_name(self, app: Optional[Any]) -> Optional[str]: # app: Optional[Atspi.Accessible] """Returns the module name of the script to use for application app.""" if app is None: @@ -143,19 +148,19 @@ class ScriptManager: _log_tokens(["Mapped", app, "to", name]) return name - def _toolkit_for_object(self, obj): + def _toolkit_for_object(self, obj: Optional[Any]) -> str: # obj: Optional[Atspi.Accessible] """Returns the name of the toolkit associated with obj.""" name = AXObject.get_attribute(obj, 'toolkit') return self._toolkitNames.get(name, name) - def _script_for_role(self, obj): + def _script_for_role(self, obj: Optional[Any]) -> str: # obj: Optional[Atspi.Accessible] if _get_ax_utilities().is_terminal(obj): return 'terminal' return '' - def _new_named_script(self, app, name): + def _new_named_script(self, app: Optional[Any], name: Optional[str]) -> Optional[Any]: # Returns Optional[Script] """Attempts to locate and load the named module. If successful, returns a script based on this module.""" @@ -184,7 +189,7 @@ class ScriptManager: return script - def _create_script(self, app, obj=None): + def _create_script(self, app: Optional[Any], obj: Optional[Any] = None) -> Any: # Returns Script """For the given application, create a new script instance.""" moduleName = self.get_module_name(app) @@ -207,7 +212,7 @@ class ScriptManager: return script - def get_default_script(self, app=None): + def get_default_script(self, app: Optional[Any] = None) -> Any: # Returns Script if not app and self._defaultScript: return self._defaultScript @@ -219,7 +224,7 @@ class ScriptManager: return script - def sanity_check_script(self, script): + def sanity_check_script(self, script: Any) -> Any: # Returns Script if not self._active: return script @@ -233,7 +238,7 @@ class ScriptManager: _log_tokens(["Failed to get a replacement script for", script.app], "replacement-missing") return script - def get_script_for_mouse_button_event(self, event): + def get_script_for_mouse_button_event(self, event: Any) -> Any: # Returns Script isActive = _get_ax_utilities().is_active(cthulhu_state.activeWindow) _log_tokens([cthulhu_state.activeWindow, "is active:", isActive]) @@ -251,10 +256,10 @@ class ScriptManager: return self.get_script(AXObject.get_application(activeWindow), activeWindow) - def get_active_script(self): + def get_active_script(self) -> Optional[Any]: # Returns Optional[Script] return cthulhu_state.activeScript - def get_script(self, app, obj=None, sanity_check=False): + def get_script(self, app: Optional[Any], obj: Optional[Any] = None, sanity_check: bool = False) -> Any: # Returns Script """Get a script for an app (and make it if necessary). This is used instead of a simple calls to Script's constructor. @@ -312,19 +317,19 @@ class ScriptManager: return appScript - def get_or_create_sleep_mode_script(self, app): + def get_or_create_sleep_mode_script(self, app: Any) -> Any: # Returns Script """Gets or creates the sleep mode script.""" script = self._sleepModeScripts.get(app) if script is not None: return script - + # Import sleepmode dynamically to avoid circular imports from .scripts import sleepmode script = sleepmode.Script(app) self._sleepModeScripts[app] = script return script - def set_active_script(self, newScript, reason=None): + def set_active_script(self, newScript: Optional[Any], reason: Optional[str] = None) -> None: # newScript: Optional[Script] """Set the new active script. Arguments: @@ -343,20 +348,20 @@ class ScriptManager: return newScript.activate() - + # Emit signal that active script has changed, so PluginSystemManager can update keybindings from . import cthulhu cthulhu.cthulhuApp.getSignalManager().emitSignal('active-script-changed', newScript) - + _log_tokens(["Setting active script to", newScript], reason) self._log_active_state(reason) - def activate_script_for_context(self, app, obj, reason=None): + def activate_script_for_context(self, app: Optional[Any], obj: Optional[Any], reason: Optional[str] = None) -> Any: # Returns Script script = self.get_script(app, obj) self.set_active_script(script, reason) return script - def _log_active_state(self, reason=None): + def _log_active_state(self, reason: Optional[str] = None) -> None: _log_tokens( ["Active state:", "window", cthulhu_state.activeWindow, "focus", cthulhu_state.locusOfFocus, @@ -364,7 +369,7 @@ class ScriptManager: reason ) - def _get_script_for_app_replicant(self, app): + def _get_script_for_app_replicant(self, app: Any) -> Optional[Any]: # Returns Optional[Script] if not self._active: return None @@ -384,7 +389,7 @@ class ScriptManager: return None - def reclaim_scripts(self): + def reclaim_scripts(self) -> None: """Compares the list of known scripts to the list of known apps, deleting any scripts as necessary. """ @@ -432,8 +437,9 @@ class ScriptManager: del app -_manager = None -def get_manager(): +_manager: Optional[ScriptManager] = None + +def get_manager() -> ScriptManager: """Returns the Script Manager""" global _manager diff --git a/src/cthulhu/script_utilities.py b/src/cthulhu/script_utilities.py index 2a02e00..05453d0 100644 --- a/src/cthulhu/script_utilities.py +++ b/src/cthulhu/script_utilities.py @@ -28,6 +28,8 @@ been pulled out from the scripts because certain scripts had gotten way too large as a result of including these methods.""" +from __future__ import annotations + __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" @@ -42,6 +44,7 @@ import re import subprocess import time from difflib import SequenceMatcher +from typing import Any, Callable, Generator, Optional, TYPE_CHECKING gi.require_version("Atspi", "2.0") from gi.repository import Atspi @@ -73,15 +76,18 @@ from .ax_value import AXValue from .ax_utilities import AXUtilities from .ax_utilities_relation import AXUtilitiesRelation -_settingsManager = None # Removed - use cthulhu.cthulhuApp.settingsManager +if TYPE_CHECKING: + from .script import Script + +_settingsManager: Optional[Any] = None # Removed - use cthulhu.cthulhuApp.settingsManager # Try to import sound system for indentation beeps try: from . import sound from .sound_generator import Tone - _SOUND_AVAILABLE = True + _SOUND_AVAILABLE: bool = True except ImportError: - _SOUND_AVAILABLE = False + _SOUND_AVAILABLE: bool = False ############################################################################# # # @@ -91,24 +97,24 @@ except ImportError: class Utilities: - _last_clipboard_update = time.time() + _last_clipboard_update: float = time.time() - EMBEDDED_OBJECT_CHARACTER = '\ufffc' - ZERO_WIDTH_NO_BREAK_SPACE = '\ufeff' - SUPERSCRIPT_DIGITS = \ + EMBEDDED_OBJECT_CHARACTER: str = '\ufffc' + ZERO_WIDTH_NO_BREAK_SPACE: str = '\ufeff' + SUPERSCRIPT_DIGITS: list[str] = \ ['\u2070', '\u00b9', '\u00b2', '\u00b3', '\u2074', '\u2075', '\u2076', '\u2077', '\u2078', '\u2079'] - SUBSCRIPT_DIGITS = \ + SUBSCRIPT_DIGITS: list[str] = \ ['\u2080', '\u2081', '\u2082', '\u2083', '\u2084', '\u2085', '\u2086', '\u2087', '\u2088', '\u2089'] - MENU_ROLES_IN_OPEN_MENU = { + MENU_ROLES_IN_OPEN_MENU: set[Atspi.Role] = { Atspi.Role.MENU, Atspi.Role.MENU_ITEM, Atspi.Role.CHECK_MENU_ITEM, Atspi.Role.RADIO_MENU_ITEM, Atspi.Role.SEPARATOR, } - ZOMBIE_TOP_LEVEL_ROLES = { + ZOMBIE_TOP_LEVEL_ROLES: set[Atspi.Role] = { Atspi.Role.APPLICATION, Atspi.Role.ALERT, Atspi.Role.DIALOG, @@ -117,45 +123,45 @@ class Utilities: Atspi.Role.WINDOW, Atspi.Role.FRAME, } - DISPLAYED_TEXT_DIRECT_NAME_ROLES = { + DISPLAYED_TEXT_DIRECT_NAME_ROLES: set[Atspi.Role] = { Atspi.Role.PUSH_BUTTON, Atspi.Role.LABEL, } - DISPLAYED_TEXT_SKIP_NAME_ROLES = { + DISPLAYED_TEXT_SKIP_NAME_ROLES: set[Atspi.Role] = { Atspi.Role.COMBO_BOX, Atspi.Role.SPIN_BUTTON, } - DISPLAYED_TEXT_LABEL_FALLBACK_ROLES = { + DISPLAYED_TEXT_LABEL_FALLBACK_ROLES: set[Atspi.Role] = { Atspi.Role.PUSH_BUTTON, Atspi.Role.LIST_ITEM, } - flags = re.UNICODE - WORDS_RE = re.compile(r"(\W+)", flags) - SUPERSCRIPTS_RE = re.compile(f"[{''.join(SUPERSCRIPT_DIGITS)}]+", flags) - SUBSCRIPTS_RE = re.compile(f"[{''.join(SUBSCRIPT_DIGITS)}]+", flags) - PUNCTUATION = re.compile(r"[^\w\s]", flags) + flags: re.RegexFlag = re.UNICODE + WORDS_RE: re.Pattern[str] = re.compile(r"(\W+)", flags) + SUPERSCRIPTS_RE: re.Pattern[str] = re.compile(f"[{''.join(SUPERSCRIPT_DIGITS)}]+", flags) + SUBSCRIPTS_RE: re.Pattern[str] = re.compile(f"[{''.join(SUBSCRIPT_DIGITS)}]+", flags) + PUNCTUATION: re.Pattern[str] = re.compile(r"[^\w\s]", flags) # generatorCache # - DISPLAYED_DESCRIPTION = 'displayedDescription' - DISPLAYED_LABEL = 'displayedLabel' - DISPLAYED_TEXT = 'displayedText' - KEY_BINDING = 'keyBinding' - NESTING_LEVEL = 'nestingLevel' - NODE_LEVEL = 'nodeLevel' + DISPLAYED_DESCRIPTION: str = 'displayedDescription' + DISPLAYED_LABEL: str = 'displayedLabel' + DISPLAYED_TEXT: str = 'displayedText' + KEY_BINDING: str = 'keyBinding' + NESTING_LEVEL: str = 'nestingLevel' + NODE_LEVEL: str = 'nodeLevel' - def __init__(self, script): + def __init__(self, script: Script) -> None: """Creates an instance of the Utilities class. Arguments: - script: the script with which this instance is associated. """ - self._script = script - self._clipboardHandlerId = None - self._lastIndentationData = {} - self._selectedMenuBarMenu = {} + self._script: Script = script + self._clipboardHandlerId: Optional[int] = None + self._lastIndentationData: dict[Any, Any] = {} + self._selectedMenuBarMenu: dict[Any, Any] = {} ######################################################################### # # @@ -163,7 +169,7 @@ class Utilities: # # ######################################################################### - def _isActiveAndShowingAndNotIconified(self, obj): + def _isActiveAndShowingAndNotIconified(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_active(obj): tokens = ["SCRIPT UTILITIES:", obj, "lacks state active"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -182,7 +188,7 @@ class Utilities: return True @staticmethod - def _getAppCommandLine(app): + def _getAppCommandLine(app: Optional[Atspi.Accessible]) -> str: if not app: return "" @@ -200,7 +206,7 @@ class Utilities: return cmdline.replace("\x00", " ") - def canBeActiveWindow(self, window, clearCache=False): + def canBeActiveWindow(self, window: Optional[Atspi.Accessible], clearCache: bool = False) -> bool: if not window: return False @@ -220,7 +226,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return True - def activeWindow(self, *apps): + def activeWindow(self, *apps: Atspi.Accessible) -> Optional[Atspi.Accessible]: """Tries to locate the active window; may or may not succeed.""" candidates = [] @@ -286,10 +292,10 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return guess - def objectAttributes(self, obj, useCache=True): + def objectAttributes(self, obj: Atspi.Accessible, useCache: bool = True) -> dict[str, str]: return AXObject.get_attributes_dict(obj) - def cellIndex(self, obj): + def cellIndex(self, obj: Atspi.Accessible) -> int: """Returns the index of the cell which should be used with the table interface. This is necessary because in some apps we cannot count on the index in parent being the index we need. @@ -306,7 +312,7 @@ class Utilities: obj = AXObject.find_ancestor(obj, AXUtilities.is_table_cell_or_header) or obj return AXObject.get_index_in_parent(obj) - def childNodes(self, obj): + def childNodes(self, obj: Atspi.Accessible) -> list[Atspi.Accessible]: """Gets all of the children that have RELATION_NODE_CHILD_OF pointing to this expanded table cell. @@ -327,7 +333,7 @@ class Utilities: # First see if this accessible implements RELATION_NODE_PARENT_OF. # If it does, the full target list are the nodes. If it doesn't # we'll do an old-school, row-by-row search for child nodes. - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return AXObject.get_index_in_parent(x) >= 0 nodes = [x for x in AXUtilitiesRelation.get_is_node_parent_of(obj) if pred(x)] @@ -360,7 +366,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return nodes - def commonAncestor(self, a, b): + def commonAncestor(self, a: Optional[Atspi.Accessible], b: Optional[Atspi.Accessible]) -> Optional[Atspi.Accessible]: """Finds the common ancestor between Accessible a and Accessible b. Arguments: @@ -407,7 +413,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return commonAncestor - def displayedLabel(self, obj): + def displayedLabel(self, obj: Atspi.Accessible) -> Optional[str]: """If there is an object labelling the given object, return the text being displayed for the object labelling this object. Otherwise, return None. @@ -434,10 +440,10 @@ class Utilities: self._script.generatorCache[self.DISPLAYED_LABEL][obj] = labelString return self._script.generatorCache[self.DISPLAYED_LABEL][obj] - def preferDescriptionOverName(self, obj): + def preferDescriptionOverName(self, obj: Atspi.Accessible) -> bool: return False - def descriptionsForObject(self, obj): + def descriptionsForObject(self, obj: Atspi.Accessible) -> list[Atspi.Accessible]: """Return a list of objects describing obj.""" descriptions = AXUtilitiesRelation.get_is_described_by(obj) @@ -453,11 +459,11 @@ class Utilities: return descriptions - def detailsContentForObject(self, obj): + def detailsContentForObject(self, obj: Atspi.Accessible) -> list[str]: details = self.detailsForObject(obj) return list(map(self.displayedText, details)) - def detailsForObject(self, obj, textOnly=True): + def detailsForObject(self, obj: Atspi.Accessible, textOnly: bool = True) -> list[Atspi.Accessible]: """Return a list of objects containing details for obj.""" details = AXUtilitiesRelation.get_details(obj) @@ -474,7 +480,7 @@ class Utilities: return textObjects - def displayedDescription(self, obj): + def displayedDescription(self, obj: Atspi.Accessible) -> str: """Returns the text being displayed for the object describing obj.""" try: @@ -487,7 +493,7 @@ class Utilities: self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj] = string return self._script.generatorCache[self.DISPLAYED_DESCRIPTION][obj] - def displayedText(self, obj): + def displayedText(self, obj: Atspi.Accessible) -> Optional[str]: """Returns the text being displayed for an object. Arguments: @@ -523,12 +529,12 @@ class Utilities: self._script.generatorCache[self.DISPLAYED_TEXT][obj] = displayedText return self._script.generatorCache[self.DISPLAYED_TEXT][obj] - def _getDisplayedTextDirectName(self, role, name): + def _getDisplayedTextDirectName(self, role: Atspi.Role, name: str) -> Optional[str]: if role in self.DISPLAYED_TEXT_DIRECT_NAME_ROLES and name: return name return None - def _getDisplayedTextFromText(self, obj): + def _getDisplayedTextFromText(self, obj: Atspi.Accessible) -> Optional[str]: if not AXObject.supports_text(obj): return None @@ -538,14 +544,14 @@ class Utilities: return displayedText - def _getDisplayedTextFallbackName(self, role, name): + def _getDisplayedTextFallbackName(self, role: Atspi.Role, name: str) -> Optional[str]: # NOTE: Legacy fallback; removal requires thorough testing. if name and role not in self.DISPLAYED_TEXT_SKIP_NAME_ROLES: return name return None - def _getDisplayedTextFromLabels(self, obj, role): + def _getDisplayedTextFromLabels(self, obj: Atspi.Accessible, role: Atspi.Role) -> Optional[str]: if role not in self.DISPLAYED_TEXT_LABEL_FALLBACK_ROLES: return None @@ -554,7 +560,7 @@ class Utilities: labels = self.unrelatedLabels(obj, onlyShowing=False, minimumWords=1) return " ".join(map(self.displayedText, labels)) - def documentFrame(self, obj=None): + def documentFrame(self, obj: Optional[Atspi.Accessible] = None) -> Optional[Atspi.Accessible]: """Returns the document frame which is displaying the content. Note that this is intended primarily for web content.""" @@ -567,12 +573,12 @@ class Utilities: return document - def documentFrameURI(self, documentFrame=None): + def documentFrameURI(self, documentFrame: Optional[Atspi.Accessible] = None) -> Optional[str]: """Returns the URI of the document frame that is active.""" return None - def frameAndDialog(self, obj): + def frameAndDialog(self, obj: Optional[Atspi.Accessible]) -> list[Optional[Atspi.Accessible]]: """Returns the frame and (possibly) the dialog containing obj.""" results = [None, None] @@ -600,7 +606,7 @@ class Utilities: if role in [Atspi.Role.FRAME, Atspi.Role.WINDOW]: results[0] = topLevel - def isDialog(x): + def isDialog(x: Atspi.Accessible) -> bool: return AXObject.get_role(x) in dialog_roles if isDialog(obj): @@ -612,13 +618,13 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return results - def presentEventFromNonShowingObject(self, event): + def presentEventFromNonShowingObject(self, event: Any) -> bool: if event.source == cthulhu_state.locusOfFocus: return True return False - def grabFocusBeforeRouting(self, obj): + def grabFocusBeforeRouting(self, obj: Atspi.Accessible) -> bool: """Whether or not we should perform a grabFocus before routing the cursor via the braille cursor routing keys. @@ -632,7 +638,7 @@ class Utilities: return AXUtilities.is_combo_box(obj) \ and not self.isSameObject(obj, cthulhu_state.locusOfFocus) - def hasMatchingHierarchy(self, obj, rolesList): + def hasMatchingHierarchy(self, obj: Atspi.Accessible, rolesList: list[Atspi.Role]) -> bool: """Called to determine if the given object and it's hierarchy of parent objects, each have the desired roles. Please note: You should strongly consider an alternative means for determining @@ -670,7 +676,7 @@ class Utilities: return True - def inFindContainer(self, obj=None): + def inFindContainer(self, obj: Optional[Atspi.Accessible] = None) -> bool: if obj is None: obj = cthulhu_state.locusOfFocus @@ -679,48 +685,48 @@ class Utilities: return AXObject.find_ancestor(obj, AXUtilities.is_tool_bar) is not None - def getFindResultsCount(self, root=None): + def getFindResultsCount(self, root: Optional[Atspi.Accessible] = None) -> str: return "" - def isAnchor(self, obj): + def isAnchor(self, obj: Atspi.Accessible) -> bool: return False - def isCode(self, obj): + def isCode(self, obj: Atspi.Accessible) -> bool: return False - def isCodeDescendant(self, obj): + def isCodeDescendant(self, obj: Atspi.Accessible) -> bool: return False - def isDockedFrame(self, obj): + def isDockedFrame(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_frame(obj): return False attrs = self.objectAttributes(obj) return attrs.get('window-type') == 'dock' - def isDesktop(self, obj): + def isDesktop(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_frame(obj): return False attrs = self.objectAttributes(obj) return attrs.get('is-desktop') == 'true' - def isComboBoxWithToggleDescendant(self, obj): + def isComboBoxWithToggleDescendant(self, obj: Atspi.Accessible) -> bool: return False - def isToggleDescendantOfComboBox(self, obj): + def isToggleDescendantOfComboBox(self, obj: Atspi.Accessible) -> bool: return False - def isTypeahead(self, obj): + def isTypeahead(self, obj: Atspi.Accessible) -> bool: return False - def isOrDescendsFrom(self, obj, ancestor): + def isOrDescendsFrom(self, obj: Atspi.Accessible, ancestor: Atspi.Accessible) -> bool: if obj == ancestor: return True return AXObject.find_ancestor(obj, lambda x: x and x == ancestor) - def isFunctionalDialog(self, obj): + def isFunctionalDialog(self, obj: Atspi.Accessible) -> bool: """Returns True if the window is a functioning as a dialog. This method should be subclassed by application scripts as needed. @@ -728,298 +734,298 @@ class Utilities: return False - def isComment(self, obj): + def isComment(self, obj: Atspi.Accessible) -> bool: return False - def isContentDeletion(self, obj): + def isContentDeletion(self, obj: Atspi.Accessible) -> bool: return False - def isContentError(self, obj): + def isContentError(self, obj: Atspi.Accessible) -> bool: return False - def isContentInsertion(self, obj): + def isContentInsertion(self, obj: Atspi.Accessible) -> bool: return False - def isContentMarked(self, obj): + def isContentMarked(self, obj: Atspi.Accessible) -> bool: return False - def isContentSuggestion(self, obj): + def isContentSuggestion(self, obj: Atspi.Accessible) -> bool: return False - def isInlineSuggestion(self, obj): + def isInlineSuggestion(self, obj: Atspi.Accessible) -> bool: return False - def isFirstItemInInlineContentSuggestion(self, obj): + def isFirstItemInInlineContentSuggestion(self, obj: Atspi.Accessible) -> bool: return False - def isLastItemInInlineContentSuggestion(self, obj): + def isLastItemInInlineContentSuggestion(self, obj: Atspi.Accessible) -> bool: return False - def isEmpty(self, obj): + def isEmpty(self, obj: Atspi.Accessible) -> bool: return False - def isHidden(self, obj): + def isHidden(self, obj: Atspi.Accessible) -> bool: return False - def isDPub(self, obj): + def isDPub(self, obj: Atspi.Accessible) -> bool: return False - def isDPubAbstract(self, obj): + def isDPubAbstract(self, obj: Atspi.Accessible) -> bool: return False - def isDPubAcknowledgments(self, obj): + def isDPubAcknowledgments(self, obj: Atspi.Accessible) -> bool: return False - def isDPubAfterword(self, obj): + def isDPubAfterword(self, obj: Atspi.Accessible) -> bool: return False - def isDPubAppendix(self, obj): + def isDPubAppendix(self, obj: Atspi.Accessible) -> bool: return False - def isDPubBibliography(self, obj): + def isDPubBibliography(self, obj: Atspi.Accessible) -> bool: return False - def isDPubBacklink(self, obj): + def isDPubBacklink(self, obj: Atspi.Accessible) -> bool: return False - def isDPubBiblioref(self, obj): + def isDPubBiblioref(self, obj: Atspi.Accessible) -> bool: return False - def isDPubChapter(self, obj): + def isDPubChapter(self, obj: Atspi.Accessible) -> bool: return False - def isDPubColophon(self, obj): + def isDPubColophon(self, obj: Atspi.Accessible) -> bool: return False - def isDPubConclusion(self, obj): + def isDPubConclusion(self, obj: Atspi.Accessible) -> bool: return False - def isDPubCover(self, obj): + def isDPubCover(self, obj: Atspi.Accessible) -> bool: return False - def isDPubCredit(self, obj): + def isDPubCredit(self, obj: Atspi.Accessible) -> bool: return False - def isDPubCredits(self, obj): + def isDPubCredits(self, obj: Atspi.Accessible) -> bool: return False - def isDPubDedication(self, obj): + def isDPubDedication(self, obj: Atspi.Accessible) -> bool: return False - def isDPubEndnote(self, obj): + def isDPubEndnote(self, obj: Atspi.Accessible) -> bool: return False - def isDPubEndnotes(self, obj): + def isDPubEndnotes(self, obj: Atspi.Accessible) -> bool: return False - def isDPubEpigraph(self, obj): + def isDPubEpigraph(self, obj: Atspi.Accessible) -> bool: return False - def isDPubEpilogue(self, obj): + def isDPubEpilogue(self, obj: Atspi.Accessible) -> bool: return False - def isDPubErrata(self, obj): + def isDPubErrata(self, obj: Atspi.Accessible) -> bool: return False - def isDPubExample(self, obj): + def isDPubExample(self, obj: Atspi.Accessible) -> bool: return False - def isDPubFootnote(self, obj): + def isDPubFootnote(self, obj: Atspi.Accessible) -> bool: return False - def isDPubForeword(self, obj): + def isDPubForeword(self, obj: Atspi.Accessible) -> bool: return False - def isDPubGlossary(self, obj): + def isDPubGlossary(self, obj: Atspi.Accessible) -> bool: return False - def isDPubGlossref(self, obj): + def isDPubGlossref(self, obj: Atspi.Accessible) -> bool: return False - def isDPubIndex(self, obj): + def isDPubIndex(self, obj: Atspi.Accessible) -> bool: return False - def isDPubIntroduction(self, obj): + def isDPubIntroduction(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPagelist(self, obj): + def isDPubPagelist(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPagebreak(self, obj): + def isDPubPagebreak(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPart(self, obj): + def isDPubPart(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPreface(self, obj): + def isDPubPreface(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPrologue(self, obj): + def isDPubPrologue(self, obj: Atspi.Accessible) -> bool: return False - def isDPubPullquote(self, obj): + def isDPubPullquote(self, obj: Atspi.Accessible) -> bool: return False - def isDPubQna(self, obj): + def isDPubQna(self, obj: Atspi.Accessible) -> bool: return False - def isDPubSubtitle(self, obj): + def isDPubSubtitle(self, obj: Atspi.Accessible) -> bool: return False - def isDPubToc(self, obj): + def isDPubToc(self, obj: Atspi.Accessible) -> bool: return False - def isFeed(self, obj): + def isFeed(self, obj: Atspi.Accessible) -> bool: return False - def isFeedArticle(self, obj): + def isFeedArticle(self, obj: Atspi.Accessible) -> bool: return False - def isFigure(self, obj): + def isFigure(self, obj: Atspi.Accessible) -> bool: return False - def isGrid(self, obj): + def isGrid(self, obj: Atspi.Accessible) -> bool: return False - def isGridCell(self, obj): + def isGridCell(self, obj: Atspi.Accessible) -> bool: return False - def isLandmark(self, obj): + def isLandmark(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkWithoutType(self, obj): + def isLandmarkWithoutType(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkBanner(self, obj): + def isLandmarkBanner(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkComplementary(self, obj): + def isLandmarkComplementary(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkContentInfo(self, obj): + def isLandmarkContentInfo(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkForm(self, obj): + def isLandmarkForm(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkMain(self, obj): + def isLandmarkMain(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkNavigation(self, obj): + def isLandmarkNavigation(self, obj: Atspi.Accessible) -> bool: return False - def isDPubNoteref(self, obj): + def isDPubNoteref(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkRegion(self, obj): + def isLandmarkRegion(self, obj: Atspi.Accessible) -> bool: return False - def isLandmarkSearch(self, obj): + def isLandmarkSearch(self, obj: Atspi.Accessible) -> bool: return False - def isSVG(self, obj): + def isSVG(self, obj: Atspi.Accessible) -> bool: return False - def speakMathSymbolNames(self, obj=None): + def speakMathSymbolNames(self, obj: Optional[Atspi.Accessible] = None) -> bool: return False - def isInMath(self): + def isInMath(self) -> bool: return False - def isMath(self, obj): + def isMath(self, obj: Atspi.Accessible) -> bool: return False - def isMathLayoutOnly(self, obj): + def isMathLayoutOnly(self, obj: Atspi.Accessible) -> bool: return False - def isMathMultiline(self, obj): + def isMathMultiline(self, obj: Atspi.Accessible) -> bool: return False - def isMathEnclosed(self, obj): + def isMathEnclosed(self, obj: Atspi.Accessible) -> bool: return False - def isMathFenced(self, obj): + def isMathFenced(self, obj: Atspi.Accessible) -> bool: return False - def isMathFractionWithoutBar(self, obj): + def isMathFractionWithoutBar(self, obj: Atspi.Accessible) -> bool: return False - def isMathPhantom(self, obj): + def isMathPhantom(self, obj: Atspi.Accessible) -> bool: return False - def isMathMultiScript(self, obj): + def isMathMultiScript(self, obj: Atspi.Accessible) -> bool: return False - def isMathSubOrSuperScript(self, obj): + def isMathSubOrSuperScript(self, obj: Atspi.Accessible) -> bool: return False - def isMathUnderOrOverScript(self, obj): + def isMathUnderOrOverScript(self, obj: Atspi.Accessible) -> bool: return False - def isMathSquareRoot(self, obj): + def isMathSquareRoot(self, obj: Atspi.Accessible) -> bool: return False - def isMathTable(self, obj): + def isMathTable(self, obj: Atspi.Accessible) -> bool: return False - def isMathTableRow(self, obj): + def isMathTableRow(self, obj: Atspi.Accessible) -> bool: return False - def isMathTableCell(self, obj): + def isMathTableCell(self, obj: Atspi.Accessible) -> bool: return False - def isMathToken(self, obj): + def isMathToken(self, obj: Atspi.Accessible) -> bool: return False - def isMathTopLevel(self, obj): + def isMathTopLevel(self, obj: Atspi.Accessible) -> bool: return False - def getMathDenominator(self, obj): + def getMathDenominator(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathNumerator(self, obj): + def getMathNumerator(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathRootBase(self, obj): + def getMathRootBase(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathRootIndex(self, obj): + def getMathRootIndex(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptBase(self, obj): + def getMathScriptBase(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptSubscript(self, obj): + def getMathScriptSubscript(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptSuperscript(self, obj): + def getMathScriptSuperscript(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptUnderscript(self, obj): + def getMathScriptUnderscript(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathScriptOverscript(self, obj): + def getMathScriptOverscript(self, obj: Atspi.Accessible) -> Optional[Atspi.Accessible]: return None - def getMathPrescripts(self, obj): + def getMathPrescripts(self, obj: Atspi.Accessible) -> list[Atspi.Accessible]: return [] - def getMathPostscripts(self, obj): + def getMathPostscripts(self, obj: Atspi.Accessible) -> list[Atspi.Accessible]: return [] - def getMathEnclosures(self, obj): + def getMathEnclosures(self, obj: Atspi.Accessible) -> list[str]: return [] - def getMathFencedSeparators(self, obj): + def getMathFencedSeparators(self, obj: Atspi.Accessible) -> list[str]: return [''] - def getMathFences(self, obj): + def getMathFences(self, obj: Atspi.Accessible) -> list[str]: return ['', ''] - def getMathNestingLevel(self, obj, test=None): + def getMathNestingLevel(self, obj: Atspi.Accessible, test: Optional[Callable[[Atspi.Accessible], bool]] = None) -> int: return 0 - def getLandmarkTypes(self): + def getLandmarkTypes(self) -> list[str]: return ["banner", "complementary", "contentinfo", @@ -1048,7 +1054,7 @@ class Utilities: "region", "search"] - def isProgressBar(self, obj): + def isProgressBar(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_progress_bar(obj): return False @@ -1066,7 +1072,7 @@ class Utilities: return True - def isProgressBarUpdate(self, obj): + def isProgressBarUpdate(self, obj: Atspi.Accessible) -> bool: if not cthulhu.cthulhuApp.settingsManager.getSetting('speakProgressBarUpdates') \ and not cthulhu.cthulhuApp.settingsManager.getSetting('brailleProgressBarUpdates') \ and not cthulhu.cthulhuApp.settingsManager.getSetting('beepProgressBarUpdates'): @@ -1100,7 +1106,7 @@ class Utilities: return True, "Not handled by any other case" - def getValueAsPercent(self, obj): + def getValueAsPercent(self, obj: Atspi.Accessible) -> Optional[Any]: if not AXObject.supports_value(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiValue"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -1123,19 +1129,19 @@ class Utilities: return int((val / (maxval - minval)) * 100) - def isBlockquote(self, obj): + def isBlockquote(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_block_quote(obj) - def isDescriptionList(self, obj): + def isDescriptionList(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_description_list(obj) - def isDescriptionListTerm(self, obj): + def isDescriptionListTerm(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_description_term(obj) - def isDescriptionListDescription(self, obj): + def isDescriptionListDescription(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_description_value(obj) - def descriptionListTerms(self, obj): + def descriptionListTerms(self, obj: Atspi.Accessible) -> list[Any]: if not self.isDescriptionList(obj): return [] @@ -1143,36 +1149,36 @@ class Utilities: _exclude = self.isDescriptionList return self.findAllDescendants(obj, _include, _exclude) - def isDocumentList(self, obj): + def isDocumentList(self, obj: Atspi.Accessible) -> bool: if AXObject.get_role(obj) not in [Atspi.Role.LIST, Atspi.Role.DESCRIPTION_LIST]: return False return AXObject.find_ancestor(obj, AXUtilities.is_document) is not None - def isDocumentPanel(self, obj): + def isDocumentPanel(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_panel(obj): return False return AXObject.find_ancestor(obj, AXUtilities.is_document) is not None - def isDocument(self, obj): + def isDocument(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_document(obj) - def inDocumentContent(self, obj=None): + def inDocumentContent(self, obj: Optional[Atspi.Accessible] = None) -> Optional[Atspi.Accessible]: obj = obj or cthulhu_state.locusOfFocus return self.getDocumentForObject(obj) is not None - def activeDocument(self, window=None): + def activeDocument(self, window: Optional[Atspi.Accessible] = None) -> Optional[Atspi.Accessible]: return self.getTopLevelDocumentForObject(cthulhu_state.locusOfFocus) - def isTopLevelDocument(self, obj): + def isTopLevelDocument(self, obj: Atspi.Accessible) -> bool: return self.isDocument(obj) and not AXObject.find_ancestor(obj, self.isDocument) - def getTopLevelDocumentForObject(self, obj): + def getTopLevelDocumentForObject(self, obj: Atspi.Accessible) -> Any: if self.isTopLevelDocument(obj): return obj return AXObject.find_ancestor(obj, self.isTopLevelDocument) - def getDocumentForObject(self, obj): + def getDocumentForObject(self, obj: Atspi.Accessible) -> Optional[Any]: if not obj: return None @@ -1181,7 +1187,7 @@ class Utilities: return AXObject.find_ancestor(obj, self.isDocument) - def getModalDialog(self, obj): + def getModalDialog(self, obj: Atspi.Accessible) -> bool: if not obj: return False @@ -1190,17 +1196,17 @@ class Utilities: return AXObject.find_ancestor(obj, AXUtilities.is_modal_dialog) - def isModalDialogDescendant(self, obj): + def isModalDialogDescendant(self, obj: Atspi.Accessible) -> bool: if not obj: return False return self.getModalDialog(obj) is not None - def getTable(self, obj): + def getTable(self, obj: Atspi.Accessible) -> Optional[Any]: if not obj: return None - def isTable(x): + def isTable(x: Atspi.Accessible) -> bool: if AXUtilities.is_table(x) or AXUtilities.is_tree_table(x) or AXUtilities.is_tree(x): return AXObject.supports_table(x) return False @@ -1210,17 +1216,17 @@ class Utilities: return AXObject.find_ancestor(obj, isTable) - def isTextDocumentTable(self, obj): + def isTextDocumentTable(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_table(obj): return False doc = self.getDocumentForObject(obj) return doc is not None and not AXUtilities.is_document_spreadsheet(doc) - def isGUITable(self, obj): + def isGUITable(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_table(obj) and self.getDocumentForObject(obj) is None - def isSpreadSheetTable(self, obj): + def isSpreadSheetTable(self, obj: Atspi.Accessible) -> bool: if not (AXUtilities.is_table(obj) and AXObject.supports_table(obj)): return False @@ -1232,17 +1238,17 @@ class Utilities: return AXTable.get_row_count(obj, prefer_attribute=False) > 65536 - def isTextDocumentCell(self, obj): + def isTextDocumentCell(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_table_cell_or_header(obj): return False return AXObject.find_ancestor(obj, self.isTextDocumentTable) - def isSpreadSheetCell(self, obj): + def isSpreadSheetCell(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_table_cell_or_header(obj): return False return AXObject.find_ancestor(obj, self.isSpreadSheetTable) - def cellColumnChanged(self, cell): + def cellColumnChanged(self, cell: Any) -> bool: row, column = self.coordinatesForCell(cell) if column == -1: return False @@ -1250,7 +1256,7 @@ class Utilities: lastColumn = self._script.pointOfReference.get("lastColumn") return column != lastColumn - def cellRowChanged(self, cell): + def cellRowChanged(self, cell: Any) -> bool: row, column = self.coordinatesForCell(cell) if row == -1: return False @@ -1258,7 +1264,7 @@ class Utilities: lastRow = self._script.pointOfReference.get("lastRow") return row != lastRow - def shouldReadFullRow(self, obj): + def shouldReadFullRow(self, obj: Atspi.Accessible) -> bool: if self._script.inSayAll(): return False @@ -1277,16 +1283,16 @@ class Utilities: return cthulhu.cthulhuApp.settingsManager.getSetting('readFullRowInDocumentTable') - def isSorted(self, obj): + def isSorted(self, obj: Atspi.Accessible) -> bool: return False - def isAscending(self, obj): + def isAscending(self, obj: Atspi.Accessible) -> bool: return False - def isDescending(self, obj): + def isDescending(self, obj: Atspi.Accessible) -> bool: return False - def getSortOrderDescription(self, obj, includeName=False): + def getSortOrderDescription(self, obj: Atspi.Accessible, includeName: Any = False) -> str: if not (obj and self.isSorted(obj)): return "" @@ -1302,18 +1308,18 @@ class Utilities: return result - def isFocusableLabel(self, obj): + def isFocusableLabel(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_label(obj) and AXUtilities.is_focusable(obj) - def isNonFocusableList(self, obj): + def isNonFocusableList(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_list(obj) and not AXUtilities.is_focusable(obj) - def isStatusBarNotification(self, obj): + def isStatusBarNotification(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_notification(obj): return False return AXObject.find_ancestor(obj, AXUtilities.is_status_bar) is not None - def getNotificationContent(self, obj): + def getNotificationContent(self, obj: Atspi.Accessible) -> str: if not AXUtilities.is_notification(obj): return "" @@ -1336,7 +1342,7 @@ class Utilities: return " ".join(tokens) - def isTreeDescendant(self, obj): + def isTreeDescendant(self, obj: Atspi.Accessible) -> bool: if obj is None: return False @@ -1345,7 +1351,7 @@ class Utilities: return AXObject.find_ancestor(obj, AXUtilities.is_tree_or_tree_table) is not None - def isLayoutOnly(self, obj): + def isLayoutOnly(self, obj: Atspi.Accessible) -> bool: """Returns True if the given object is a container which has no presentable information (label, name, displayed text, etc.).""" @@ -1442,8 +1448,7 @@ class Utilities: return layoutOnly - @staticmethod - def isInActiveApp(obj): + def isInActiveApp(self, obj: Atspi.Accessible) -> bool: """Returns True if the given object is from the same application that currently has keyboard focus. @@ -1456,12 +1461,12 @@ class Utilities: return AXObject.get_application(cthulhu_state.locusOfFocus) == AXObject.get_application(obj) - def isLink(self, obj): + def isLink(self, obj: Atspi.Accessible) -> bool: """Returns True if obj is a link.""" return AXUtilities.is_link(obj) - def isReadOnlyTextArea(self, obj): + def isReadOnlyTextArea(self, obj: Atspi.Accessible) -> bool: """Returns True if obj is a text entry area that is read only.""" if not self.isTextArea(obj): @@ -1472,10 +1477,10 @@ class Utilities: return AXUtilities.is_focusable(obj) and not AXUtilities.is_editable(obj) - def isSwitch(self, obj): + def isSwitch(self, obj: Atspi.Accessible) -> bool: return False - def get_objectFromPath(self, path): + def get_objectFromPath(self, path: int) -> Any: start = self._script.app rv = None for p in path: @@ -1490,7 +1495,7 @@ class Utilities: return rv - def _hasSamePath(self, obj1, obj2): + def _hasSamePath(self, obj1: Any, obj2: Any) -> bool: path1 = AXObject.get_path(obj1) path2 = AXObject.get_path(obj2) if len(path1) != len(path2): @@ -1524,8 +1529,9 @@ class Utilities: return path1[0:index] == path2[0:index] - def isSameObject(self, obj1, obj2, comparePaths=False, ignoreNames=False, - ignoreDescriptions=True): + def isSameObject(self, obj1: Optional[Atspi.Accessible], obj2: Optional[Atspi.Accessible], + comparePaths: bool = False, ignoreNames: bool = False, + ignoreDescriptions: bool = True) -> bool: if obj1 == obj2: return True @@ -1572,7 +1578,7 @@ class Utilities: return False - def isTextArea(self, obj): + def isTextArea(self, obj: Atspi.Accessible) -> bool: """Returns True if obj is a GUI component that is for entering text. Arguments: @@ -1584,22 +1590,22 @@ class Utilities: return self._isTextAreaByType(obj) - def _isTextAreaByType(self, obj): + def _isTextAreaByType(self, obj: Atspi.Accessible) -> Any: # NOTE: This is legacy and may need more checks now. return AXUtilities.is_text_input(obj) \ or AXUtilities.is_text(obj) \ or AXUtilities.is_paragraph(obj) - def labelsForObject(self, obj): + def labelsForObject(self, obj: Atspi.Accessible) -> Any: """Return a list of the labels for this object.""" - def isNotAncestor(acc): + def isNotAncestor(acc: Atspi.Accessible) -> bool: return not AXObject.find_ancestor(obj, lambda x: x == acc) result = AXUtilitiesRelation.get_is_labelled_by(obj) return list(filter(isNotAncestor, result)) - def linkBasenameToName(self, obj): + def linkBasenameToName(self, obj: Atspi.Accessible) -> str: basename = self.linkBasename(obj) if not basename: return "" @@ -1612,8 +1618,7 @@ class Utilities: return basename - @staticmethod - def linkBasename(obj): + def linkBasename(self, obj: Atspi.Accessible) -> Any: """Returns the relevant information from the URI. The idea is to attempt to strip off all prefix and suffix, much like the basename command in a shell.""" @@ -1666,8 +1671,7 @@ class Utilities: return basename - @staticmethod - def linkIndex(obj, characterIndex): + def linkIndex(self, obj: Atspi.Accessible, characterIndex: int) -> int: """A brute force method to see if an offset is a link. This is provided because not all Accessible Hypertext implementations properly support the getLinkIndex method. Returns an index of @@ -1695,7 +1699,7 @@ class Utilities: return -1 - def nestingLevel(self, obj): + def nestingLevel(self, obj: Atspi.Accessible) -> int: """Determines the nesting level of this object. Arguments: @@ -1711,7 +1715,7 @@ class Utilities: if self.NESTING_LEVEL not in self._script.generatorCache: self._script.generatorCache[self.NESTING_LEVEL] = {} - def pred(x): + def pred(x: Atspi.Accessible) -> bool: if self.isBlockquote(obj): return self.isBlockquote(x) if AXUtilities.is_list_item(obj): @@ -1728,7 +1732,7 @@ class Utilities: self._script.generatorCache[self.NESTING_LEVEL][obj] = nestingLevel return self._script.generatorCache[self.NESTING_LEVEL][obj] - def nodeLevel(self, obj): + def nodeLevel(self, obj: Atspi.Accessible) -> int: """Determines the node level of this object if it is in a tree relation, with 0 being the top level node. If this object is not in a tree relation, then -1 will be returned. @@ -1772,7 +1776,7 @@ class Utilities: self._script.generatorCache[self.NODE_LEVEL][obj] = len(nodes) - 1 return self._script.generatorCache[self.NODE_LEVEL][obj] - def isOnScreen(self, obj, boundingbox=None): + def isOnScreen(self, obj: Atspi.Accessible, boundingbox: Any = None) -> bool: if AXObject.is_dead(obj): return False @@ -1835,7 +1839,7 @@ class Utilities: return True - def selectedMenuBarMenu(self, menubar): + def selectedMenuBarMenu(self, menubar: Any) -> Optional[Any]: if not AXUtilities.is_menu_bar(menubar): return None @@ -1850,9 +1854,9 @@ class Utilities: if AXUtilities.is_expanded(menu) or AXUtilities.is_selected(menu): return menu - return None + return - def isInOpenMenuBarMenu(self, obj): + def isInOpenMenuBarMenu(self, obj: Atspi.Accessible) -> bool: if obj is None: return False @@ -1867,7 +1871,7 @@ class Utilities: if selectedMenu is None: return False - def inSelectedMenu(x): + def inSelectedMenu(x: Atspi.Accessible) -> bool: return x == selectedMenu if inSelectedMenu(obj): @@ -1875,13 +1879,13 @@ class Utilities: return AXObject.find_ancestor(obj, inSelectedMenu) is not None - def isStaticTextLeaf(self, obj): + def isStaticTextLeaf(self, obj: Atspi.Accessible) -> bool: return False - def isListItemMarker(self, obj): + def isListItemMarker(self, obj: Atspi.Accessible) -> bool: return False - def hasPresentableText(self, obj): + def hasPresentableText(self, obj: Atspi.Accessible) -> bool: if self.isStaticTextLeaf(obj): return False @@ -1890,7 +1894,7 @@ class Utilities: return bool(re.search(r"\w+", AXText.get_all_text(obj))) - def getOnScreenObjects(self, root, extents=None): + def getOnScreenObjects(self, root: Any, extents: Any = None) -> list[Any]: if not self.isOnScreen(root, extents): return [] @@ -1938,7 +1942,7 @@ class Utilities: elif self.hasPresentableText(root): objects.append(root) - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return x is not None and not self.isStaticTextLeaf(x) for child in AXObject.iter_children(root, pred): @@ -1967,8 +1971,7 @@ class Utilities: return [root] - @staticmethod - def isTableRow(obj): + def isTableRow(self, obj: Atspi.Accessible) -> bool: """Determines if obj is a table row -- real or functionally.""" childCount = AXObject.get_child_count(obj) @@ -1993,11 +1996,11 @@ class Utilities: return False - def realActiveAncestor(self, obj): + def realActiveAncestor(self, obj: Atspi.Accessible) -> Any: if AXUtilities.is_focused(obj): return obj - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return AXUtilities.is_table_cell_or_header(x) or AXUtilities.is_list_item(x) ancestor = AXObject.find_ancestor(obj, pred) @@ -2007,7 +2010,7 @@ class Utilities: return obj - def realActiveDescendant(self, obj): + def realActiveDescendant(self, obj: Atspi.Accessible) -> Optional[Any]: """Given an object that should be a child of an object that manages its descendants, return the child that is the real active descendant carrying useful information. @@ -2026,7 +2029,7 @@ class Utilities: if AXObject.get_name(obj): return obj - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return AXObject.get_name(x) or AXText.get_all_text(x) child = AXObject.find_descendant(obj, pred) @@ -2035,13 +2038,13 @@ class Utilities: return obj - def isStatusBarDescendant(self, obj): + def isStatusBarDescendant(self, obj: Atspi.Accessible) -> bool: if obj is None: return False return AXObject.find_ancestor(obj, AXUtilities.is_status_bar) is not None - def statusBarItems(self, obj): + def statusBarItems(self, obj: Atspi.Accessible) -> list[Any]: if not AXUtilities.is_status_bar(obj): return [] @@ -2049,7 +2052,7 @@ class Utilities: items = self._script.pointOfReference.get('statusBarItems') if not items: - def include(x): + def include(x: Atspi.Accessible) -> bool: return not AXUtilities.is_status_bar(x) items = list(filter(include, self.getOnScreenObjects(obj))) @@ -2061,10 +2064,10 @@ class Utilities: return items - def infoBar(self, root): + def infoBar(self, root: Any) -> Optional[Any]: return None - def _topLevelRoles(self): + def _topLevelRoles(self) -> Any: roles = [Atspi.Role.DIALOG, Atspi.Role.FILE_CHOOSER, Atspi.Role.FRAME, @@ -2073,7 +2076,7 @@ class Utilities: roles.append(Atspi.Role.ALERT) return roles - def _locusOfFocusIsTopLevelObject(self): + def _locusOfFocusIsTopLevelObject(self) -> bool: if not cthulhu_state.locusOfFocus: return False @@ -2082,7 +2085,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return rv - def _findWindowWithDescendant(self, child): + def _findWindowWithDescendant(self, child: Any) -> Optional[Any]: """Searches each frame/window/dialog of an application to find the one which contains child. This is extremely non-performant and should only be used to work around broken accessibility trees where topLevelObject @@ -2104,11 +2107,11 @@ class Utilities: return None - def _isTopLevelObject(self, obj): + def _isTopLevelObject(self, obj: Atspi.Accessible) -> Any: return AXObject.get_role(obj) in self._topLevelRoles() \ and AXObject.get_role(AXObject.get_parent(obj)) == Atspi.Role.APPLICATION - def topLevelObject(self, obj, useFallbackSearch=False): + def topLevelObject(self, obj: Atspi.Accessible, useFallbackSearch: bool = False) -> Any: """Returns the top-level object (frame, dialog ...) containing obj, or None if obj is not inside a top-level object. @@ -2131,7 +2134,7 @@ class Utilities: return rv - def topLevelObjectIsActiveAndCurrent(self, obj=None): + def topLevelObjectIsActiveAndCurrent(self, obj: Optional[Atspi.Accessible] = None) -> bool: obj = obj or cthulhu_state.locusOfFocus topLevel = self.topLevelObject(obj) if not topLevel: @@ -2146,8 +2149,7 @@ class Utilities: return True - @staticmethod - def onSameLine(obj1, obj2, delta=0): + def onSameLine(self, obj1: Atspi.Accessible, obj2: Atspi.Accessible, delta: int = 0) -> bool: """Determines if obj1 and obj2 are on the same line.""" if not AXObject.supports_component(obj1) or not AXObject.supports_component(obj2): @@ -2164,8 +2166,7 @@ class Utilities: return abs(center1 - center2) <= delta - @staticmethod - def pathComparison(path1, path2): + def pathComparison(self, path1: int, path2: int) -> int: """Compares the two paths and returns -1, 0, or 1 to indicate if path1 is before, the same, or after path2.""" @@ -2184,8 +2185,7 @@ class Utilities: return 0 - @staticmethod - def sizeComparison(obj1, obj2): + def sizeComparison(self, obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> Any: width1, height1 = 0, 0 width2, height2 = 0, 0 @@ -2205,8 +2205,7 @@ class Utilities: return (width1 * height1) - (width2 * height2) - @staticmethod - def spatialComparison(obj1, obj2): + def spatialComparison(self, obj1: Atspi.Accessible, obj2: Atspi.Accessible) -> Any: """Compares the physical locations of obj1 and obj2 and returns -1, 0, or 1 to indicate if obj1 physically is before, is in the same place as, or is after obj2.""" @@ -2242,7 +2241,7 @@ class Utilities: return rv - def getTextBoundingBox(self, obj, start, end): + def getTextBoundingBox(self, obj: Atspi.Accessible, start: int, end: int) -> int: if not AXObject.supports_text(obj): return -1, -1, 0, 0 @@ -2255,7 +2254,7 @@ class Utilities: return extents - def getBoundingBox(self, obj): + def getBoundingBox(self, obj: Atspi.Accessible) -> int: if not AXObject.supports_component(obj): return -1, -1, 0, 0 @@ -2268,7 +2267,7 @@ class Utilities: return extents.x, extents.y, extents.width, extents.height - def hasNoSize(self, obj): + def hasNoSize(self, obj: Atspi.Accessible) -> bool: if not obj: return False @@ -2287,10 +2286,10 @@ class Utilities: return not (extents.width and extents.height) - def findAllDescendants(self, root, includeIf=None, excludeIf=None): + def findAllDescendants(self, root: Any, includeIf: Any = None, excludeIf: Any = None) -> Any: return AXObject.find_all_descendants(root, includeIf, excludeIf) - def unrelatedLabels(self, root, onlyShowing=True, minimumWords=3): + def unrelatedLabels(self, root: Any, onlyShowing: Any = True, minimumWords: Any = 3) -> list[Any]: """Returns a list containing all the unrelated (i.e., have no relations to anything and are not a fundamental element of a more atomic component like a combo box) labels under the given @@ -2317,7 +2316,7 @@ class Utilities: Atspi.Role.TREE, Atspi.Role.TREE_TABLE] - def _include(x): + def _include(x: Atspi.Accessible) -> bool: if not (x and AXObject.get_role(x) in labelRoles): return False if AXUtilitiesRelation.get_relations(x): @@ -2326,7 +2325,7 @@ class Utilities: return False return True - def _exclude(x): + def _exclude(x: Atspi.Accessible) -> bool: if not x or AXObject.get_role(x) in skipRoles: return True if onlyShowing and not AXUtilities.is_showing(x): @@ -2352,10 +2351,10 @@ class Utilities: return sorted(labels, key=functools.cmp_to_key(self.spatialComparison)) - def _treatAlertsAsDialogs(self): + def _treatAlertsAsDialogs(self) -> bool: return True - def unfocusedAlertAndDialogCount(self, obj): + def unfocusedAlertAndDialogCount(self, obj: Atspi.Accessible) -> Any: """If the current application has one or more alert or dialog windows and the currently focused window is not an alert or a dialog, return a count of the number of alert and dialog windows, otherwise @@ -2371,24 +2370,24 @@ class Utilities: if self._treatAlertsAsDialogs(): roles.append(Atspi.Role.ALERT) - def isDialog(x): + def isDialog(x: Atspi.Accessible) -> bool: return AXObject.get_role(x) in roles or self.isFunctionalDialog(x) dialogs = [x for x in AXObject.iter_children(AXObject.get_application(obj), isDialog)] dialogs.extend([x for x in AXObject.iter_children(self.topLevelObject(obj), isDialog)]) - def isPresentable(x): + def isPresentable(x: Atspi.Accessible) -> bool: return self.isShowingAndVisible(x) \ and (AXObject.get_name(x) or AXObject.get_child_count(x)) - def cannotBeActiveWindow(x): + def cannotBeActiveWindow(x: Atspi.Accessible) -> bool: return not self.canBeActiveWindow(x) presentable = list(filter(isPresentable, set(dialogs))) unfocused = list(filter(cannotBeActiveWindow, presentable)) return len(unfocused) - def uri(self, obj): + def uri(self, obj: Atspi.Accessible) -> Any: """Return the URI for a given link object. Arguments: @@ -2404,8 +2403,7 @@ class Utilities: # # ######################################################################### - @staticmethod - def adjustTextSelection(obj, offset): + def adjustTextSelection(self, obj: Atspi.Accessible, offset: int) -> None: """Adjusts the end point of a text selection Arguments: @@ -2431,7 +2429,7 @@ class Utilities: AXText.set_selected_text(obj, startOffset, endOffset) - def findPreviousObject(self, obj): + def findPreviousObject(self, obj: Atspi.Accessible) -> Optional[Any]: """Finds the object before this one.""" if not obj or self.isZombie(obj): @@ -2443,7 +2441,7 @@ class Utilities: return AXUtilities.get_previous_object(obj) - def findNextObject(self, obj): + def findNextObject(self, obj: Atspi.Accessible) -> Optional[Any]: """Finds the object after this one.""" if not obj or self.isZombie(obj): @@ -2455,7 +2453,7 @@ class Utilities: return AXUtilities.get_next_object(obj) - def allSelectedText(self, obj): + def allSelectedText(self, obj: Atspi.Accessible) -> Any: """Get all the text applicable text selections for the given object. including any previous or next text objects that also have selected text and add in their text contents. @@ -2494,8 +2492,7 @@ class Utilities: return textContents, startOffset, endOffset - @staticmethod - def allTextSelections(obj): + def allTextSelections(self, obj: Atspi.Accessible) -> Any: """Get a list of text selections in the given accessible object, equivalent to getNSelections()*texti.getSelection() @@ -2509,7 +2506,7 @@ class Utilities: return AXText.get_selected_ranges(obj) - def getChildAtOffset(self, obj, offset): + def getChildAtOffset(self, obj: Atspi.Accessible, offset: Any) -> Optional[Any]: child = AXHypertext.get_child_at_offset(obj, offset) if not child: return None @@ -2518,7 +2515,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return child - def findChildAtOffset(self, obj, offset): + def findChildAtOffset(self, obj: Atspi.Accessible, offset: int) -> Any: """Attempts to correct for off-by-one brokenness in hypertext implementations. We're seeing off-by-one errors in (at least) Chromium where the text @@ -2551,7 +2548,7 @@ class Utilities: return None - def characterOffsetInParent(self, obj): + def characterOffsetInParent(self, obj: Atspi.Accessible) -> Any: """Returns the character offset of the embedded object character for this object in its parent's accessible text. @@ -2580,7 +2577,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return offset - def clearTextSelection(self, obj): + def clearTextSelection(self, obj: Atspi.Accessible) -> None: """Clears the text selection if the object supports it. Arguments: @@ -2593,11 +2590,11 @@ class Utilities: for i in range(AXText._get_n_selections(obj)): AXText._remove_selection(obj, i) - def containsOnlyEOCs(self, obj): + def containsOnlyEOCs(self, obj: Atspi.Accessible) -> Any: string = AXText.get_all_text(obj) return string and not re.search(r"[^\ufffc]", string) - def expandEOCs(self, obj, startOffset=0, endOffset=-1): + def expandEOCs(self, obj: Atspi.Accessible, startOffset: Any = 0, endOffset: Any = -1) -> str: """Expands the current object replacing EMBEDDED_OBJECT_CHARACTERS with their text. @@ -2637,7 +2634,7 @@ class Utilities: return "".join(toBuild) - def isWordMisspelled(self, obj, offset): + def isWordMisspelled(self, obj: Atspi.Accessible, offset: Any) -> bool: """Identifies if the current word is flagged as misspelled by the application. Different applications and toolkits flag misspelled words differently. Thus each script will likely need to implement @@ -2661,16 +2658,16 @@ class Utilities: return False - def getError(self, obj): + def getError(self, obj: Atspi.Accessible) -> Any: return AXUtilities.is_invalid_entry(obj) - def getErrorMessage(self, obj): + def getErrorMessage(self, obj: Atspi.Accessible) -> str: return "" - def isErrorMessage(self, obj): + def isErrorMessage(self, obj: Atspi.Accessible) -> bool: return False - def getCharacterAtOffset(self, obj, offset=None): + def getCharacterAtOffset(self, obj: Atspi.Accessible, offset: int = None) -> Any: if AXObject.supports_text(obj): if offset is None: offset = AXText.get_caret_offset(obj) @@ -2678,7 +2675,7 @@ class Utilities: return "" - def queryNonEmptyText(self, obj): + def queryNonEmptyText(self, obj: Atspi.Accessible) -> Optional[Any]: """Get the text interface associated with an object, if it is non-empty. @@ -2695,10 +2692,10 @@ class Utilities: return None - def deletedText(self, event): + def deletedText(self, event: Any) -> Any: return event.any_data - def insertedText(self, event): + def insertedText(self, event: Any) -> Any: if event.any_data: return event.any_data @@ -2713,7 +2710,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return "" - def _getFallbackInsertedText(self, event): + def _getFallbackInsertedText(self, event: Any) -> str: if not AXUtilities.is_password_text(event.source): return "" @@ -2729,7 +2726,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return string[-1] - def selectedText(self, obj): + def selectedText(self, obj: Atspi.Accessible) -> Any: """Get the text selection for the given object. Arguments: @@ -2754,7 +2751,7 @@ class Utilities: return [textContents, startOffset, endOffset] - def getCaretContext(self): + def getCaretContext(self) -> Any: obj = cthulhu_state.locusOfFocus if AXObject.supports_text(obj): offset = AXText.get_caret_offset(obj) @@ -2763,14 +2760,14 @@ class Utilities: return obj, offset - def getFirstCaretPosition(self, obj): + def getFirstCaretPosition(self, obj: Atspi.Accessible) -> Any: return obj, 0 - def setCaretPosition(self, obj, offset, documentFrame=None): + def setCaretPosition(self, obj: Atspi.Accessible, offset: int, documentFrame: Any = None) -> Any: cthulhu.setLocusOfFocus(None, obj, False) self.setCaretOffset(obj, offset) - def setCaretOffset(self, obj, offset): + def setCaretOffset(self, obj: Atspi.Accessible, offset: int) -> Any: """Set the caret offset on a given accessible. Similar to Accessible.setCaretOffset() @@ -2780,7 +2777,7 @@ class Utilities: """ AXText.set_caret_offset(obj, offset) - def substring(self, obj, startOffset, endOffset): + def substring(self, obj: Atspi.Accessible, startOffset: Any, endOffset: Any) -> Any: """Returns the substring of the given object's text specialization. Arguments: @@ -2792,7 +2789,7 @@ class Utilities: return AXText.get_substring(obj, startOffset, endOffset) - def getAppNameForAttribute(self, attribName): + def getAppNameForAttribute(self, attribName: Any) -> Any: """Converts the given Atk attribute name into the application's equivalent. This is necessary because an application or toolkit (e.g. Gecko) might invent entirely new names for the same text @@ -2811,7 +2808,7 @@ class Utilities: return attribName - def getAtkNameForAttribute(self, attribName): + def getAtkNameForAttribute(self, attribName: Any) -> Any: """Converts the given attribute name into the Atk equivalent. This is necessary because an application or toolkit (e.g. Gecko) might invent entirely new names for the same attributes. @@ -2824,7 +2821,7 @@ class Utilities: return self._script.attributeNamesDict.get(attribName, attribName) - def getAllTextAttributesForObject(self, obj, startOffset=0, endOffset=-1): + def getAllTextAttributesForObject(self, obj: Atspi.Accessible, startOffset: Any = 0, endOffset: Any = -1) -> list[Any]: """Returns a list of (start, end, attrsDict) tuples for obj.""" if not AXObject.supports_text(obj): return [] @@ -2851,7 +2848,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return rv - def textAttributes(self, acc, offset=None, get_defaults=False): + def textAttributes(self, acc: Any, offset: Any = None, get_defaults: Any = False) -> dict[Any, Any]: """Get the text attributes run for a given offset in a given accessible Arguments: @@ -2875,7 +2872,7 @@ class Utilities: attrs, start, end = AXText.get_text_attributes_at_offset(acc, offset) return attrs, min(start, offset), max(end, offset + 1) - def localizeTextAttribute(self, key, value): + def localizeTextAttribute(self, key: Any, value: Any) -> Any: if key == "weight" and (value == "bold" or int(value) > 400): return messages.BOLD @@ -2903,7 +2900,7 @@ class Utilities: return f"{localizedKey}: {localizedValue}" - def splitSubstringByLanguage(self, obj, start, end): + def splitSubstringByLanguage(self, obj: Atspi.Accessible, start: int, end: int) -> Any: """Returns a list of (start, end, string, language, dialect) tuples.""" rv = [] @@ -2920,7 +2917,7 @@ class Utilities: return rv - def getLanguageAndDialectForSubstring(self, obj, start, end): + def getLanguageAndDialectForSubstring(self, obj: Atspi.Accessible, start: int, end: int) -> Any: """Returns a (language, dialect) tuple. If multiple languages apply to the substring, language and dialect will be empty strings. Callers must do any preprocessing to avoid that condition.""" @@ -2932,7 +2929,7 @@ class Utilities: return "", "" - def getLanguageAndDialectFromTextAttributes(self, obj, startOffset=0, endOffset=-1): + def getLanguageAndDialectFromTextAttributes(self, obj: Atspi.Accessible, startOffset: Any = 0, endOffset: Any = -1) -> Any: """Returns a list of (start, end, language, dialect) tuples for obj based on what is exposed via text attributes.""" @@ -2952,7 +2949,7 @@ class Utilities: return rv - def willEchoCharacter(self, event): + def willEchoCharacter(self, event: Any) -> bool: """Given a keyboard event containing an alphanumeric key, determine if the script is likely to echo it as a character. """ @@ -2978,7 +2975,7 @@ class Utilities: # # ######################################################################### - def _addRepeatSegment(self, segment, line, respectPunctuation=True): + def _addRepeatSegment(self, segment: Any, line: Any, respectPunctuation: Any = True) -> Any: """Add in the latest line segment, adjusting for repeat characters and punctuation. @@ -3014,7 +3011,7 @@ class Utilities: return line - def shouldVerbalizeAllPunctuation(self, obj): + def shouldVerbalizeAllPunctuation(self, obj: Atspi.Accessible) -> bool: if not (self.isCode(obj) or self.isCodeDescendant(obj)): return False @@ -3027,7 +3024,7 @@ class Utilities: return True - def verbalizeAllPunctuation(self, string): + def verbalizeAllPunctuation(self, string: Any) -> Any: result = string for symbol in set(re.findall(self.PUNCTUATION, result)): charName = f" {chnames.getCharacterName(symbol)} " @@ -3035,7 +3032,7 @@ class Utilities: return result - def adjustForLinks(self, obj, line, startOffset): + def adjustForLinks(self, obj: Atspi.Accessible, line: Any, startOffset: Any) -> Any: """Adjust line to include the word "link" after any hypertext links. Arguments: @@ -3085,18 +3082,16 @@ class Utilities: return "".join(adjustedLine) - @staticmethod - def _processMultiCaseString(string): + def _processMultiCaseString(self, string: Any) -> Any: return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string) - @staticmethod - def _convertWordToDigits(word): + def _convertWordToDigits(self, word: Any) -> Any: if not word.isnumeric(): return word return ' '.join(list(word)) - def adjustForPronunciation(self, line): + def adjustForPronunciation(self, line: Any) -> Any: """Adjust the line to replace words in the pronunciation dictionary, with what those words actually sound like. @@ -3134,7 +3129,7 @@ class Utilities: return newLine - def adjustForRepeats(self, line): + def adjustForRepeats(self, line: Any) -> Any: """Adjust line to include repeat character counts. As some people will want this and others might not, there is a setting in settings.py that determines whether this functionality is enabled. @@ -3172,7 +3167,7 @@ class Utilities: return self._addRepeatSegment(segment, newLine, multipleChars) - def adjustForDigits(self, string): + def adjustForDigits(self, string: Any) -> Any: """Adjusts the string to convert digit-like text, such as subscript and superscript numbers, into actual digits. @@ -3205,7 +3200,7 @@ class Utilities: _INDENTATION_DURATION = 0.15 _INDENTATION_VOLUME = 0.7 - def _get_indentation_key(self, obj): + def _get_indentation_key(self, obj: Atspi.Accessible) -> Any: if obj is None: return "global" @@ -3214,8 +3209,7 @@ class Utilities: except Exception: return str(obj) - @staticmethod - def _extract_indentation(line): + def _extract_indentation(self, line: Any) -> str: if not line: return "" @@ -3226,8 +3220,7 @@ class Utilities: return line - @staticmethod - def _get_indentation_columns(indentation, tabWidth): + def _get_indentation_columns(self, indentation: Any, tabWidth: Any) -> Any: columns = 0 tabWidth = max(1, tabWidth) for char in indentation: @@ -3238,7 +3231,7 @@ class Utilities: return columns - def _get_indentation_data(self, line): + def _get_indentation_data(self, line: Any) -> Any: indentation = self._extract_indentation(line) columns = self._get_indentation_columns(indentation, self._INDENTATION_TAB_WIDTH) return { @@ -3246,14 +3239,14 @@ class Utilities: "columns": columns, } - def _remember_indentation(self, obj, data): + def _remember_indentation(self, obj: Atspi.Accessible, data: Any) -> bool: key = self._get_indentation_key(obj) self._lastIndentationData[key] = { "signature": data["indentation"], "columns": data["columns"], } - def _indentation_has_changed(self, obj, data): + def _indentation_has_changed(self, obj: Atspi.Accessible, data: Any) -> bool: key = self._get_indentation_key(obj) previous = self._lastIndentationData.get(key) self._remember_indentation(obj, data) @@ -3264,12 +3257,12 @@ class Utilities: previousColumns = previous.get("columns", 0) return changed, previousColumns - def _indentation_enabled(self): + def _indentation_enabled(self) -> bool: if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): return False return cthulhu.cthulhuApp.settingsManager.getSetting('enableIndentation') - def _indentation_speech_enabled(self): + def _indentation_speech_enabled(self) -> bool: if not self._indentation_enabled(): return False @@ -3280,7 +3273,7 @@ class Utilities: settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, ) - def _indentation_beeps_enabled(self): + def _indentation_beeps_enabled(self) -> bool: if not self._indentation_enabled(): return False @@ -3291,7 +3284,7 @@ class Utilities: settings.INDENTATION_PRESENTATION_SPEECH_AND_BEEPS, ) - def _play_indentation_tone(self, columns, previousColumns): + def _play_indentation_tone(self, columns: Any, previousColumns: Any) -> None: """Play an audio tone indicating indentation level.""" if not _SOUND_AVAILABLE: return @@ -3325,7 +3318,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, f"INDENTATION: error playing tone: {e}", True) - def get_indentation_presentation(self, line, obj=None): + def get_indentation_presentation(self, line: Any, obj: Optional[Atspi.Accessible] = None) -> str: data = self._get_indentation_data(line) hasIndentation = bool(data["indentation"]) presentationMode = cthulhu.cthulhuApp.settingsManager.getSetting('indentationPresentationMode') \ @@ -3367,19 +3360,18 @@ class Utilities: return "", hasIndentation - def should_strip_indentation(self, line): + def should_strip_indentation(self, line: Any) -> bool: if not self._indentation_enabled(): return False data = self._get_indentation_data(line) return bool(data["indentation"]) - def indentationDescription(self, line, obj=None): + def indentationDescription(self, line: Any, obj: Optional[Atspi.Accessible] = None) -> Any: description, _hasIndentation = self.get_indentation_presentation(line, obj=obj) return description - @staticmethod - def absoluteMouseCoordinates(): + def absoluteMouseCoordinates(self) -> Any: """Gets the absolute position of the mouse pointer.""" from gi.repository import Gtk @@ -3388,8 +3380,7 @@ class Utilities: return x, y - @staticmethod - def appendString(text, newText, delimiter=" "): + def appendString(self, text: Any, newText: Any, delimiter: Any = " ") -> Any: """Appends the newText to the given text with the delimiter in between and returns the new string. Edge cases, such as no initial text or no newText, are handled gracefully.""" @@ -3401,7 +3392,7 @@ class Utilities: return text + delimiter + newText - def treatAsDuplicateEvent(self, event1, event2): + def treatAsDuplicateEvent(self, event1: Any, event2: Any) -> bool: if not (event1 and event2): return False @@ -3415,7 +3406,7 @@ class Utilities: and event1.detail2 == event2.detail2 \ and event1.any_data == event2.any_data - def isAutoTextEvent(self, event): + def isAutoTextEvent(self, event: Any) -> bool: """Returns True if event is associated with text being autocompleted or autoinserted or autocorrected or autosomethingelsed. @@ -3457,7 +3448,7 @@ class Utilities: return False - def isSentenceDelimiter(self, currentChar, previousChar): + def isSentenceDelimiter(self, currentChar: Any, previousChar: Any) -> bool: """Returns True if we are positioned at the end of a sentence. This is determined by checking if the current character is a white space character and the previous character is one of the @@ -3476,7 +3467,7 @@ class Utilities: return currentChar in self._script.whitespace \ and previousChar in '!.?:;' - def isWordDelimiter(self, character): + def isWordDelimiter(self, character: Any) -> Any: """Returns True if the given character is a word delimiter. Arguments: @@ -3489,7 +3480,7 @@ class Utilities: or character in r'!*+,-./:;<=>?@[\]^_{|}' \ or character == self._script.NO_BREAK_SPACE_CHARACTER - def intersectingRegion(self, obj1, obj2, coordType=None): + def intersectingRegion(self, obj1: Any, obj2: Any, coordType: Any = None) -> int: """Returns the extents of the intersection of obj1 and obj2.""" if coordType is None: @@ -3506,8 +3497,8 @@ class Utilities: return self.intersection(extents1, extents2) - def intersection(self, extents1, extents2): - def _toTuple(extents): + def intersection(self, extents1: Any, extents2: Any) -> int: + def _toTuple(extents: Any) -> tuple[int, int, int, int]: if extents is None: return 0, 0, 0, 0 if hasattr(extents, "x") and hasattr(extents, "y") \ @@ -3538,11 +3529,10 @@ class Utilities: return x, y, width, height - def containsRegion(self, extents1, extents2): + def containsRegion(self, extents1: Any, extents2: Any) -> Any: return self.intersection(extents1, extents2) != (0, 0, 0, 0) - @staticmethod - def _extentsToTuple(extents): + def _extentsToTuple(self, extents: Any) -> int: if extents is None: return 0, 0, 0, 0 if hasattr(extents, "x") and hasattr(extents, "y") \ @@ -3550,14 +3540,12 @@ class Utilities: return extents.x, extents.y, extents.width, extents.height return tuple(extents) - @staticmethod - def _allNamesForKeyCode(keycode): + def _allNamesForKeyCode(self, keycode: Any) -> Any: keymap = Gdk.Keymap.get_default() entries = keymap.get_entries_for_keycode(keycode)[-1] return list(map(Gdk.keyval_name, set(entries))) - @staticmethod - def _lastKeyCodeAndModifiers(): + def _lastKeyCodeAndModifiers(self) -> int: if not isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent): return 0, 0 @@ -3567,8 +3555,7 @@ class Utilities: return 0, 0 - @staticmethod - def lastKeyAndModifiers(): + def lastKeyAndModifiers(self) -> Any: """Convenience method which returns a tuple containing the event string and modifiers of the last non-modifier key event or ("", 0) if there is no such event.""" @@ -3587,8 +3574,7 @@ class Utilities: return (eventStr, mods) - @staticmethod - def labelFromKeySequence(sequence): + def labelFromKeySequence(self, sequence: Any) -> Any: """Turns a key sequence into a user-presentable label.""" try: @@ -3606,7 +3592,7 @@ class Utilities: return keynames.localizeKeySequence(sequence) - def mnemonicShortcutAccelerator(self, obj): + def mnemonicShortcutAccelerator(self, obj: Atspi.Accessible) -> Any: """Gets the mnemonic, accelerator string and possibly shortcut for the given object. These are based upon the first accessible action for the object. @@ -3665,8 +3651,7 @@ class Utilities: [mnemonic, fullShortcut, accelerator] return self._script.generatorCache[self.KEY_BINDING][obj] - @staticmethod - def stringToKeysAndDict(string): + def stringToKeysAndDict(self, string: Any) -> list[Any]: """Converts a string made up of a series of :; pairs into a dictionary of keys and values. Text before the colon is the key and text afterwards is the value. The final semi-colon, if @@ -3690,7 +3675,7 @@ class Utilities: return [keys, dictionary] - def textForValue(self, obj): + def textForValue(self, obj: Atspi.Accessible) -> Any: """Returns the text to be displayed for the object's current value. Arguments: @@ -3734,8 +3719,7 @@ class Utilities: formatter = "%%.%df" % decimalPlaces return formatter % currentValue - @staticmethod - def unicodeValueString(character): + def unicodeValueString(self, character: Any) -> Any: """ Returns a four hex digit representation of the given character Arguments: @@ -3750,7 +3734,7 @@ class Utilities: debug.printException(debug.LEVEL_WARNING) return "" - def getLinesForRange(self, obj, startOffset, endOffset): + def getLinesForRange(self, obj: Atspi.Accessible, startOffset: Any, endOffset: Any) -> list[Any]: if not AXObject.supports_text(obj): return [] @@ -3765,25 +3749,25 @@ class Utilities: return lines - def getLineContentsAtOffset(self, obj, offset, layoutMode=True, useCache=True): + def getLineContentsAtOffset(self, obj: Atspi.Accessible, offset: Any, layoutMode: Any = True, useCache: Any = True) -> list[Any]: return [] - def get_objectContentsAtOffset(self, obj, offset=0, useCache=True): + def get_objectContentsAtOffset(self, obj: Atspi.Accessible, offset: Any = 0, useCache: Any = True) -> list[Any]: return [] - def previousContext(self, obj=None, offset=-1, skipSpace=False): + def previousContext(self, obj: Optional[Atspi.Accessible] = None, offset: int = -1, skipSpace: Any = False) -> Any: if not obj: obj, offset = self.getCaretContext() return obj, offset - 1 - def nextContext(self, obj=None, offset=-1, skipSpace=False): + def nextContext(self, obj: Optional[Atspi.Accessible] = None, offset: int = -1, skipSpace: Any = False) -> Any: if not obj: obj, offset = self.getCaretContext() return obj, offset + 1 - def lastContext(self, root): + def lastContext(self, root: Any) -> Any: offset = 0 text = self.queryNonEmptyText(root) if text: @@ -3791,7 +3775,7 @@ class Utilities: return root, offset - def getHyperlinkRange(self, obj): + def getHyperlinkRange(self, obj: Atspi.Accessible) -> int: """Returns the text range in parent associated with obj.""" start = AXHypertext.get_link_start_offset(obj) @@ -3803,7 +3787,7 @@ class Utilities: return start, end - def selectedChildren(self, obj): + def selectedChildren(self, obj: Atspi.Accessible) -> Any: children = AXSelection.get_selected_children(obj) if children: return children @@ -3820,17 +3804,17 @@ class Utilities: children = self.selectedChildren(children[0]) name = AXObject.get_name(obj) if not children and name: - def pred(x): + def pred(x: Atspi.Accessible) -> bool: return AXObject.get_name(x) == name children = self.findAllDescendants(obj, pred) return children - def speakSelectedCellRange(self, obj): + def speakSelectedCellRange(self, obj: Atspi.Accessible) -> bool: return False - def getSelectionContainer(self, obj): + def getSelectionContainer(self, obj: Atspi.Accessible) -> Optional[Any]: if not obj: return None @@ -3847,14 +3831,14 @@ class Utilities: } matchingRoles = rolemap.get(AXObject.get_role(obj)) - def isMatch(x): + def isMatch(x: Atspi.Accessible) -> bool: if matchingRoles and AXObject.get_role(x) not in matchingRoles: return False return AXObject.supports_selection(x) return AXObject.find_ancestor(obj, isMatch) - def selectableChildCount(self, obj): + def selectableChildCount(self, obj: Atspi.Accessible) -> int: if not AXObject.supports_selection(obj): return 0 @@ -3871,12 +3855,12 @@ class Utilities: if role not in rolemap: return AXObject.get_child_count(obj) - def isMatch(x): + def isMatch(x: Atspi.Accessible) -> bool: return AXObject.get_role(x) in rolemap.get(role) return len(self.findAllDescendants(obj, isMatch)) - def selectedChildCount(self, obj): + def selectedChildCount(self, obj: Atspi.Accessible) -> Any: if AXObject.supports_table(obj): count = AXTable.get_selected_row_count(obj) if count: @@ -3884,7 +3868,7 @@ class Utilities: return AXSelection.get_selected_child_count(obj) - def popupMenuFor(self, obj): + def popupMenuFor(self, obj: Atspi.Accessible) -> Optional[Any]: if obj is None: return None @@ -3895,10 +3879,10 @@ class Utilities: return None - def isButtonWithPopup(self, obj): + def isButtonWithPopup(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_button(obj) and AXUtilities.has_popup(obj) - def isPopupMenuForCurrentItem(self, obj): + def isPopupMenuForCurrentItem(self, obj: Atspi.Accessible) -> bool: if obj == cthulhu_state.locusOfFocus: return False @@ -3911,13 +3895,13 @@ class Utilities: return name == AXObject.get_name(cthulhu_state.locusOfFocus) - def isMenuWithNoSelectedChild(self, obj): + def isMenuWithNoSelectedChild(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_menu(obj) and not self.selectedChildCount(obj) - def isMenuButton(self, obj): + def isMenuButton(self, obj: Atspi.Accessible) -> bool: return AXUtilities.is_button(obj) and self.popupMenuFor(obj) is not None - def inMenu(self, obj=None): + def inMenu(self, obj: Optional[Atspi.Accessible] = None) -> bool: obj = obj or cthulhu_state.locusOfFocus if obj is None: return False @@ -3930,38 +3914,38 @@ class Utilities: return False - def inContextMenu(self, obj=None): + def inContextMenu(self, obj: Optional[Atspi.Accessible] = None) -> bool: obj = obj or cthulhu_state.locusOfFocus if not self.inMenu(obj): return False return AXObject.find_ancestor(obj, self.isContextMenu) is not None - def _contextMenuParentRoles(self): + def _contextMenuParentRoles(self) -> Any: return Atspi.Role.FRAME, Atspi.Role.WINDOW - def isContextMenu(self, obj): + def isContextMenu(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_menu(obj): return False return AXObject.get_role(AXObject.get_parent(obj)) in self._contextMenuParentRoles() - def isTopLevelMenu(self, obj): + def isTopLevelMenu(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_menu(obj): return False return AXObject.get_parent(obj) == self.topLevelObject(obj) - def isSingleLineAutocompleteEntry(self, obj): + def isSingleLineAutocompleteEntry(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_entry(obj): return False if not AXUtilities.supports_autocompletion(obj): return False return AXUtilities.is_single_line(obj) - def isEntryCompletionPopupItem(self, obj): + def isEntryCompletionPopupItem(self, obj: Atspi.Accessible) -> bool: return False - def getEntryForEditableComboBox(self, obj): + def getEntryForEditableComboBox(self, obj: Atspi.Accessible) -> Optional[Any]: if not AXUtilities.is_combo_box(obj): return None @@ -3971,16 +3955,16 @@ class Utilities: return None - def isEditableComboBox(self, obj): + def isEditableComboBox(self, obj: Atspi.Accessible) -> bool: return self.getEntryForEditableComboBox(obj) is not None - def isEditableDescendantOfComboBox(self, obj): + def isEditableDescendantOfComboBox(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_editable(obj): return False return AXObject.find_ancestor(obj, AXUtilities.is_combo_box) is not None - def getComboBoxValue(self, obj): + def getComboBoxValue(self, obj: Atspi.Accessible) -> Any: if not AXObject.get_child_count(obj): return self.displayedText(obj) @@ -3995,46 +3979,46 @@ class Utilities: return self.displayedText(obj) - def isPopOver(self, obj): + def isPopOver(self, obj: Atspi.Accessible) -> bool: return False - def isNonModalPopOver(self, obj): + def isNonModalPopOver(self, obj: Atspi.Accessible) -> bool: if not self.isPopOver(obj): return False return not AXUtilities.is_modal(obj) - def isUselessPanel(self, obj): + def isUselessPanel(self, obj: Atspi.Accessible) -> bool: return False - def rgbFromString(self, attributeValue): + def rgbFromString(self, attributeValue: Any) -> Any: regex = re.compile(r"rgb|[^\w,]", re.IGNORECASE) string = re.sub(regex, "", attributeValue) red, green, blue = string.split(",") return int(red), int(green), int(blue) - def isClickableElement(self, obj): + def isClickableElement(self, obj: Atspi.Accessible) -> bool: return False - def hasLongDesc(self, obj): + def hasLongDesc(self, obj: Atspi.Accessible) -> bool: return False - def hasDetails(self, obj): + def hasDetails(self, obj: Atspi.Accessible) -> bool: return False - def isDetails(self, obj): + def isDetails(self, obj: Atspi.Accessible) -> bool: return False - def detailsFor(self, obj): + def detailsFor(self, obj: Atspi.Accessible) -> list[Any]: return [] - def hasVisibleCaption(self, obj): + def hasVisibleCaption(self, obj: Atspi.Accessible) -> bool: return False - def popupType(self, obj): + def popupType(self, obj: Atspi.Accessible) -> str: return '' - def headingLevel(self, obj): + def headingLevel(self, obj: Atspi.Accessible) -> int: if not AXUtilities.is_heading(obj): return 0 @@ -4049,17 +4033,17 @@ class Utilities: return value - def hasMeaningfulToggleAction(self, obj): + def hasMeaningfulToggleAction(self, obj: Atspi.Accessible) -> Any: return AXObject.has_action(obj, "toggle") \ or AXObject.has_action(obj, object_properties.ACTION_TOGGLE) - def containingTableHeader(self, obj): + def containingTableHeader(self, obj: Atspi.Accessible) -> Any: if AXUtilities.is_table_header(obj): return obj return AXObject.find_ancestor(obj, AXUtilities.is_table_header) - def columnHeadersForCell(self, obj): + def columnHeadersForCell(self, obj: Atspi.Accessible) -> Any: result = self._columnHeadersForCell(obj) # There either are no headers, or we got all of them. if len(result) != 1: @@ -4072,7 +4056,7 @@ class Utilities: return result - def _columnHeadersForCell(self, obj): + def _columnHeadersForCell(self, obj: Atspi.Accessible) -> list[Any]: if not obj: msg = "SCRIPT UTILITIES: Attempted to get column headers for null cell" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -4080,7 +4064,7 @@ class Utilities: return AXTable.get_column_headers(obj) - def rowHeadersForCell(self, obj): + def rowHeadersForCell(self, obj: Atspi.Accessible) -> Any: result = self._rowHeadersForCell(obj) # There either are no headers, or we got all of them. if len(result) != 1: @@ -4093,7 +4077,7 @@ class Utilities: return result - def _rowHeadersForCell(self, obj): + def _rowHeadersForCell(self, obj: Atspi.Accessible) -> list[Any]: if not obj: msg = "SCRIPT UTILITIES: Attempted to get row headers for null cell" debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -4101,39 +4085,39 @@ class Utilities: return AXTable.get_row_headers(obj) - def columnHeaderForCell(self, obj): + def columnHeaderForCell(self, obj: Atspi.Accessible) -> Any: headers = self.columnHeadersForCell(obj) if headers: return headers[0] return None - def rowHeaderForCell(self, obj): + def rowHeaderForCell(self, obj: Atspi.Accessible) -> Any: headers = self.rowHeadersForCell(obj) if headers: return headers[0] return None - def _shouldUseTableCellInterfaceForCoordinates(self): + def _shouldUseTableCellInterfaceForCoordinates(self) -> bool: return True - def coordinatesForCell(self, obj, preferAttribute=True, findCellAncestor=False): + def coordinatesForCell(self, obj: Atspi.Accessible, preferAttribute: Any = True, findCellAncestor: Any = False) -> Any: return AXTable.get_cell_coordinates( obj, prefer_attribute=preferAttribute, find_cell=findCellAncestor) - def rowAndColumnSpan(self, obj): + def rowAndColumnSpan(self, obj: Atspi.Accessible) -> int: if not AXUtilities.is_table_cell_or_header(obj): return -1, -1 return AXTable.get_cell_spans(obj, prefer_attribute=True) - def setSizeUnknown(self, obj): + def setSizeUnknown(self, obj: Atspi.Accessible) -> Any: return AXUtilities.is_indeterminate(obj) - def rowOrColumnCountUnknown(self, obj): + def rowOrColumnCountUnknown(self, obj: Atspi.Accessible) -> Any: return AXUtilities.is_indeterminate(obj) - def rowAndColumnCount(self, obj, preferAttribute=True): + def rowAndColumnCount(self, obj: Atspi.Accessible, preferAttribute: Any = True) -> int: if not AXObject.supports_table(obj): return -1, -1 @@ -4141,13 +4125,13 @@ class Utilities: cols = AXTable.get_column_count(obj, prefer_attribute=preferAttribute) return rows, cols - def _objectBoundsMightBeBogus(self, obj): + def _objectBoundsMightBeBogus(self, obj: Atspi.Accessible) -> bool: return False - def _objectMightBeBogus(self, obj): + def _objectMightBeBogus(self, obj: Atspi.Accessible) -> bool: return False - def containsPoint(self, obj, x, y, coordType, margin=2): + def containsPoint(self, obj: Atspi.Accessible, x: Any, y: Any, coordType: Any, margin: Any = 2) -> bool: if self._objectBoundsMightBeBogus(obj) \ and self.textAtPoint(obj, x, y, coordType) == ("", 0, 0): return False @@ -4175,7 +4159,7 @@ class Utilities: return False - def _boundsIncludeChildren(self, obj): + def _boundsIncludeChildren(self, obj: Atspi.Accessible) -> bool: if obj is None: return False @@ -4184,10 +4168,10 @@ class Utilities: return not (AXUtilities.is_menu(obj) or AXUtilities.is_page_tab(obj)) - def treatAsEntry(self, obj): + def treatAsEntry(self, obj: Atspi.Accessible) -> bool: return False - def _treatAsLeafNode(self, obj): + def _treatAsLeafNode(self, obj: Atspi.Accessible) -> bool: if obj is None or AXObject.is_dead(obj): return False @@ -4211,7 +4195,7 @@ class Utilities: return False - def accessibleAtPoint(self, root, x, y, coordType=None): + def accessibleAtPoint(self, root: Any, x: Any, y: Any, coordType: Any = None) -> Optional[Any]: if self.isHidden(root): return None @@ -4234,7 +4218,7 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return result - def descendantAtPoint(self, root, x, y, coordType=None): + def descendantAtPoint(self, root: Any, x: Any, y: Any, coordType: Any = None) -> Optional[Any]: if not root: return None @@ -4281,10 +4265,10 @@ class Utilities: return None - def _adjustPointForObj(self, obj, x, y, coordType): + def _adjustPointForObj(self, obj: Atspi.Accessible, x: Any, y: Any, coordType: Any) -> Any: return x, y - def isMultiParagraphObject(self, obj): + def isMultiParagraphObject(self, obj: Atspi.Accessible) -> bool: if not obj: return False @@ -4295,7 +4279,7 @@ class Utilities: chunks = list(filter(lambda x: x.strip(), string.split("\n\n"))) return len(chunks) > 1 - def getWordAtOffsetAdjustedForNavigation(self, obj, offset=None): + def getWordAtOffsetAdjustedForNavigation(self, obj: Atspi.Accessible, offset: Any = None) -> str: if not AXObject.supports_text(obj): return "", 0, 0 @@ -4377,7 +4361,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return word, start, end - def getWordAtOffset(self, obj, offset=None): + def getWordAtOffset(self, obj: Atspi.Accessible, offset: Any = None) -> str: if not AXObject.supports_text(obj): return "", 0, 0 @@ -4393,7 +4377,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return word, start, end - def textAtPoint(self, obj, x, y, coordType=None, boundary=None): + def textAtPoint(self, obj: Atspi.Accessible, x: Any, y: Any, coordType: Any = None, boundary: Any = None) -> str: text = self.queryNonEmptyText(obj) if not text: return "", 0, 0 @@ -4460,7 +4444,7 @@ class Utilities: return string, start, end - def visibleRows(self, obj, boundingbox): + def visibleRows(self, obj: Atspi.Accessible, boundingbox: Any) -> list[Any]: if not AXObject.supports_table(obj): return [] @@ -4507,7 +4491,7 @@ class Utilities: return rows - def getVisibleTableCells(self, obj): + def getVisibleTableCells(self, obj: Atspi.Accessible) -> list[Any]: if not AXObject.supports_table(obj): return [] @@ -4547,7 +4531,7 @@ class Utilities: return cells - def _getTableRowRange(self, obj): + def _getTableRowRange(self, obj: Atspi.Accessible) -> Any: rowCount, columnCount = self.rowAndColumnCount(obj) startIndex, endIndex = 0, columnCount if not self.isSpreadSheetCell(obj): @@ -4585,7 +4569,7 @@ class Utilities: return startIndex, endIndex - def getShowingCellsInSameRow(self, obj, forceFullRow=False): + def getShowingCellsInSameRow(self, obj: Atspi.Accessible, forceFullRow: Any = False) -> list[Any]: parent = self.getTable(obj) if not parent or not AXObject.supports_table(parent): tokens = ["SCRIPT UTILITIES: Exception querying table interface of", parent] @@ -4611,7 +4595,7 @@ class Utilities: return cells - def cellForCoordinates(self, obj, row, column, showingOnly=False): + def cellForCoordinates(self, obj: Atspi.Accessible, row: Any, column: Any, showingOnly: Any = False) -> Optional[Any]: if not AXObject.supports_table(obj): return None @@ -4624,7 +4608,7 @@ class Utilities: return None - def isLastCell(self, obj): + def isLastCell(self, obj: Atspi.Accessible) -> bool: if not AXUtilities.is_table_cell(obj): return False @@ -4637,13 +4621,13 @@ class Utilities: cols = AXTable.get_column_count(table, prefer_attribute=False) return row + 1 == rows and col + 1 == cols - def isNonUniformTable(self, obj, maxRows=25, maxCols=25): + def isNonUniformTable(self, obj: Atspi.Accessible, maxRows: Any = 25, maxCols: Any = 25) -> bool: if not AXObject.supports_table(obj): return False return AXTable.is_non_uniform_table(obj, maxRows, maxCols) - def isShowingAndVisible(self, obj): + def isShowingAndVisible(self, obj: Atspi.Accessible) -> bool: if AXUtilities.is_showing(obj) and AXUtilities.is_visible(obj): return True @@ -4657,12 +4641,12 @@ class Utilities: return False - def _is_open_menu_bar_menu_role(self, obj): + def _is_open_menu_bar_menu_role(self, obj: Atspi.Accessible) -> bool: if AXObject.get_role(obj) not in self.MENU_ROLES_IN_OPEN_MENU: return False return self.isInOpenMenuBarMenu(obj) - def isZombie(self, obj): + def isZombie(self, obj: Atspi.Accessible) -> bool: index = AXObject.get_index_in_parent(obj) role = AXObject.get_role(obj) tokens = ["SCRIPT UTILITIES: ", obj, "is zombie:"] @@ -4685,7 +4669,7 @@ class Utilities: return False - def findReplicant(self, root, obj): + def findReplicant(self, root: Any, obj: Atspi.Accessible) -> Optional[Any]: tokens = ["SCRIPT UTILITIES: Searching for replicant for", obj, "in", root] debug.printTokens(debug.LEVEL_INFO, tokens, True) if not (root and obj): @@ -4694,7 +4678,7 @@ class Utilities: if AXUtilities.is_table(root) or AXUtilities.is_embedded(root): return None - def isSame(x): + def isSame(x: Atspi.Accessible) -> bool: return self.isSameObject(x, obj, comparePaths=True, ignoreNames=True) if isSame(root): @@ -4706,13 +4690,13 @@ class Utilities: debug.printTokens(debug.LEVEL_INFO, tokens, True) return replicant - def getFunctionalChildCount(self, obj): + def getFunctionalChildCount(self, obj: Atspi.Accessible) -> Any: nodeParents = AXUtilitiesRelation.get_is_node_parent_of(obj) if nodeParents: return len(nodeParents) return AXObject.get_child_count(obj) - def getFunctionalChildren(self, obj, sibling=None): + def getFunctionalChildren(self, obj: Atspi.Accessible, sibling: Any = None) -> Any: result = AXUtilitiesRelation.get_is_node_parent_of(obj) if result: return result @@ -4722,13 +4706,13 @@ class Utilities: return self.valuesForTerm(self.termForValue(sibling)) return [x for x in AXObject.iter_children(obj)] - def getFunctionalParent(self, obj): + def getFunctionalParent(self, obj: Atspi.Accessible) -> Any: nodeChildren = AXUtilitiesRelation.get_is_node_child_of(obj) if nodeChildren: return nodeChildren[0] return AXObject.get_parent(obj) - def getPositionAndSetSize(self, obj, **args): + def getPositionAndSetSize(self, obj: Atspi.Accessible, **args: Any) -> int: if obj is None: return -1, -1 @@ -4742,7 +4726,7 @@ class Utilities: if selected: obj = selected[0] else: - def isMenu(x): + def isMenu(x: Atspi.Accessible) -> bool: return AXUtilities.is_menu(x) or AXUtilities.is_list_box(x) selected = self.selectedChildren(AXObject.find_descendant(obj, isMenu)) @@ -4760,7 +4744,7 @@ class Utilities: if len(siblings) < 100 and not AXObject.find_ancestor(obj, AXUtilities.is_combo_box): layoutRoles = [Atspi.Role.SEPARATOR, Atspi.Role.TEAROFF_MENU_ITEM] - def isNotLayoutOnly(x): + def isNotLayoutOnly(x: Atspi.Accessible) -> bool: return not (self.isZombie(x) or AXObject.get_role(x) in layoutRoles) siblings = list(filter(isNotLayoutOnly, siblings)) @@ -4777,7 +4761,7 @@ class Utilities: setSize = len(siblings) return position, setSize - def termForValue(self, obj): + def termForValue(self, obj: Atspi.Accessible) -> Optional[Any]: if not self.isDescriptionListDescription(obj): return None @@ -4786,7 +4770,7 @@ class Utilities: return obj - def valuesForTerm(self, obj): + def valuesForTerm(self, obj: Atspi.Accessible) -> list[Any]: if not self.isDescriptionListTerm(obj): return [] @@ -4798,20 +4782,20 @@ class Utilities: return values - def getValueCountForTerm(self, obj): + def getValueCountForTerm(self, obj: Atspi.Accessible) -> Any: return len(self.valuesForTerm(obj)) - def getRoleDescription(self, obj, isBraille=False): + def getRoleDescription(self, obj: Atspi.Accessible, isBraille: Any = False) -> str: return "" - def getCachedTextSelection(self, obj): + def getCachedTextSelection(self, obj: Atspi.Accessible) -> Any: textSelections = self._script.pointOfReference.get('textSelections', {}) start, end, string = textSelections.get(hash(obj), (0, 0, '')) tokens = ["SCRIPT UTILITIES: Cached selection for", obj, f"is '{string}' ({start}, {end})"] debug.printTokens(debug.LEVEL_INFO, tokens, True) return start, end, string - def updateCachedTextSelection(self, obj): + def updateCachedTextSelection(self, obj: Atspi.Accessible) -> Any: if not AXObject.supports_text(obj): tokens = ["SCRIPT UTILITIES:", obj, "doesn't implement AtspiText"] debug.printTokens(debug.LEVEL_INFO, tokens, True) @@ -4843,15 +4827,14 @@ class Utilities: textSelections[hash(obj)] = start, end, string self._script.pointOfReference['textSelections'] = textSelections - def _getSingleSelectionText(self, obj): + def _getSingleSelectionText(self, obj: Atspi.Accessible) -> Any: # NOTE: Does not handle multiple non-contiguous selections. string, start, end = AXText.get_selected_text(obj) if string: string = self.expandEOCs(obj, start, end) return string, start, end - @staticmethod - def onClipboardContentsChanged(*args): + def onClipboardContentsChanged(self, *args: Any) -> None: script = cthulhu_state.activeScript if not script: return @@ -4864,7 +4847,7 @@ class Utilities: Utilities._last_clipboard_update = time.time() script.onClipboardContentsChanged(*args) - def connectToClipboard(self): + def connectToClipboard(self) -> None: if self._clipboardHandlerId is not None: return @@ -4872,65 +4855,65 @@ class Utilities: self._clipboardHandlerId = clipboard.connect( 'owner-change', self.onClipboardContentsChanged) - def disconnectFromClipboard(self): + def disconnectFromClipboard(self) -> None: if self._clipboardHandlerId is None: return clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False)) clipboard.disconnect(self._clipboardHandlerId) - def getClipboardContents(self): + def getClipboardContents(self) -> Any: clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False)) return clipboard.wait_for_text() - def setClipboardText(self, text): + def setClipboardText(self, text: Any) -> bool: clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False)) clipboard.set_text(text, -1) - def appendTextToClipboard(self, text): + def appendTextToClipboard(self, text: Any) -> bool: clipboard = Gtk.Clipboard.get(Gdk.Atom.intern("CLIPBOARD", False)) clipboard.request_text(self._appendTextToClipboardCallback, text) - def _appendTextToClipboardCallback(self, clipboard, text, newText, separator="\n"): + def _appendTextToClipboardCallback(self, clipboard: Any, text: Any, newText: Any, separator: Any = "\n") -> bool: text = text.rstrip("\n") text = f"{text}{separator}{newText}" clipboard.set_text(text, -1) - def lastInputEventCameFromThisApp(self): + def lastInputEventCameFromThisApp(self) -> bool: if not isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent): return False event = cthulhu_state.lastNonModifierKeyEvent return event and event.isFromApplication(self._script.app) - def lastInputEventWasPrintableKey(self): + def lastInputEventWasPrintableKey(self) -> Any: return input_event_manager.get_manager().last_event_was_printable_key() - def lastInputEventWasCommand(self): + def lastInputEventWasCommand(self) -> Any: return input_event_manager.get_manager().last_event_was_command() - def lastInputEventWasPageSwitch(self): + def lastInputEventWasPageSwitch(self) -> Any: return input_event_manager.get_manager().last_event_was_page_switch() - def lastInputEventWasUnmodifiedArrow(self): + def lastInputEventWasUnmodifiedArrow(self) -> Any: return input_event_manager.get_manager().last_event_was_unmodified_arrow() - def lastInputEventWasCaretNav(self): + def lastInputEventWasCaretNav(self) -> Any: return input_event_manager.get_manager().last_event_was_caret_navigation() - def lastInputEventWasCharNav(self): + def lastInputEventWasCharNav(self) -> Any: return input_event_manager.get_manager().last_event_was_character_navigation() - def lastInputEventWasWordNav(self): + def lastInputEventWasWordNav(self) -> Any: return input_event_manager.get_manager().last_event_was_word_navigation() - def lastInputEventWasPrevWordNav(self): + def lastInputEventWasPrevWordNav(self) -> Any: return input_event_manager.get_manager().last_event_was_previous_word_navigation() - def lastInputEventWasNextWordNav(self): + def lastInputEventWasNextWordNav(self) -> Any: return input_event_manager.get_manager().last_event_was_next_word_navigation() - def lastInputEventWasLineNav(self): + def lastInputEventWasLineNav(self) -> bool: if not input_event_manager.get_manager().last_event_was_line_navigation(): return False @@ -4939,10 +4922,10 @@ class Utilities: return True - def lastInputEventWasLineBoundaryNav(self): + def lastInputEventWasLineBoundaryNav(self) -> Any: return input_event_manager.get_manager().last_event_was_line_boundary_navigation() - def lastInputEventWasPageNav(self): + def lastInputEventWasPageNav(self) -> bool: if not input_event_manager.get_manager().last_event_was_page_navigation(): return False @@ -4951,58 +4934,58 @@ class Utilities: return True - def lastInputEventWasFileBoundaryNav(self): + def lastInputEventWasFileBoundaryNav(self) -> Any: return input_event_manager.get_manager().last_event_was_file_boundary_navigation() - def lastInputEventWasCaretNavWithSelection(self): + def lastInputEventWasCaretNavWithSelection(self) -> Any: return input_event_manager.get_manager().last_event_was_caret_selection() - def lastInputEventWasUndo(self): + def lastInputEventWasUndo(self) -> Any: return input_event_manager.get_manager().last_event_was_undo() - def lastInputEventWasRedo(self): + def lastInputEventWasRedo(self) -> Any: return input_event_manager.get_manager().last_event_was_redo() - def lastInputEventWasCut(self): + def lastInputEventWasCut(self) -> Any: return input_event_manager.get_manager().last_event_was_cut() - def lastInputEventWasCopy(self): + def lastInputEventWasCopy(self) -> Any: return input_event_manager.get_manager().last_event_was_copy() - def lastInputEventWasPaste(self): + def lastInputEventWasPaste(self) -> Any: return input_event_manager.get_manager().last_event_was_paste() - def lastInputEventWasSelectAll(self): + def lastInputEventWasSelectAll(self) -> Any: return input_event_manager.get_manager().last_event_was_select_all() - def lastInputEventWasDelete(self): + def lastInputEventWasDelete(self) -> Any: return input_event_manager.get_manager().last_event_was_delete() - def lastInputEventWasTab(self): + def lastInputEventWasTab(self) -> Any: return input_event_manager.get_manager().last_event_was_tab() - def lastInputEventWasMouseButton(self): + def lastInputEventWasMouseButton(self) -> Any: return input_event_manager.get_manager().last_event_was_mouse_button() - def lastInputEventWasPrimaryMouseClick(self): + def lastInputEventWasPrimaryMouseClick(self) -> Any: return input_event_manager.get_manager().last_event_was_primary_click() - def lastInputEventWasMiddleMouseClick(self): + def lastInputEventWasMiddleMouseClick(self) -> Any: return input_event_manager.get_manager().last_event_was_middle_click() - def lastInputEventWasSecondaryMouseClick(self): + def lastInputEventWasSecondaryMouseClick(self) -> Any: return input_event_manager.get_manager().last_event_was_secondary_click() - def lastInputEventWasPrimaryMouseRelease(self): + def lastInputEventWasPrimaryMouseRelease(self) -> Any: return input_event_manager.get_manager().last_event_was_primary_release() - def lastInputEventWasMiddleMouseRelease(self): + def lastInputEventWasMiddleMouseRelease(self) -> Any: return input_event_manager.get_manager().last_event_was_middle_release() - def lastInputEventWasSecondaryMouseRelease(self): + def lastInputEventWasSecondaryMouseRelease(self) -> Any: return input_event_manager.get_manager().last_event_was_secondary_release() - def lastInputEventWasTableSort(self, delta=0.5): + def lastInputEventWasTableSort(self, delta: Any = 0.5) -> bool: if not input_event_manager.get_manager().last_event_was_table_sort(): return False @@ -5029,7 +5012,7 @@ class Utilities: return AXUtilities.is_table_header(cthulhu_state.locusOfFocus) - def isPresentableExpandedChangedEvent(self, event): + def isPresentableExpandedChangedEvent(self, event: Any) -> bool: if self.isSameObject(event.source, cthulhu_state.locusOfFocus): return True @@ -5041,7 +5024,7 @@ class Utilities: return False - def isPresentableTextChangedEventForLocusOfFocus(self, event): + def isPresentableTextChangedEventForLocusOfFocus(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:") \ and not event.type.startswith("object:text-attributes-changed"): return False @@ -5073,7 +5056,7 @@ class Utilities: debug.printMessage(debug.LEVEL_INFO, msg, True) return False - def isBackSpaceCommandTextDeletionEvent(self, event): + def isBackSpaceCommandTextDeletionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:delete"): return False @@ -5086,13 +5069,13 @@ class Utilities: return False - def isDeleteCommandTextDeletionEvent(self, event): + def isDeleteCommandTextDeletionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:delete"): return False return self.lastInputEventWasDelete() - def isUndoCommandTextDeletionEvent(self, event): + def isUndoCommandTextDeletionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:delete"): return False @@ -5102,7 +5085,7 @@ class Utilities: start, end, string = self.getCachedTextSelection(event.source) return not string - def isSelectedTextDeletionEvent(self, event): + def isSelectedTextDeletionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:delete"): return False @@ -5112,7 +5095,7 @@ class Utilities: start, end, string = self.getCachedTextSelection(event.source) return string and string.strip() == event.any_data.strip() - def isSelectedTextInsertionEvent(self, event): + def isSelectedTextInsertionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:insert"): return False @@ -5120,7 +5103,7 @@ class Utilities: start, end, string = self.getCachedTextSelection(event.source) return string and string == event.any_data and start == event.detail1 - def isSelectedTextRestoredEvent(self, event): + def isSelectedTextRestoredEvent(self, event: Any) -> bool: if not self.lastInputEventWasUndo(): return False @@ -5129,13 +5112,13 @@ class Utilities: return False - def isMiddleMouseButtonTextInsertionEvent(self, event): + def isMiddleMouseButtonTextInsertionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:insert"): return False return self.lastInputEventWasMiddleMouseClick() - def isEchoableTextInsertionEvent(self, event): + def isEchoableTextInsertionEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed:insert"): return False @@ -5155,12 +5138,12 @@ class Utilities: return False - def isEditableTextArea(self, obj): + def isEditableTextArea(self, obj: Atspi.Accessible) -> bool: if not self.isTextArea(obj): return False return AXUtilities.is_editable(obj) - def isClipboardTextChangedEvent(self, event): + def isClipboardTextChangedEvent(self, event: Any) -> bool: if not event.type.startswith("object:text-changed"): return False @@ -5189,14 +5172,14 @@ class Utilities: return False - def _isParagraphClipboardEvent(self, event, contents): + def _isParagraphClipboardEvent(self, event: Any, contents: Any) -> bool: # NOTE: Paragraph-per-object toolkits can emit per-paragraph events. if "\n" in contents and event.any_data.rstrip() in contents: return True return False - def objectContentsAreInClipboard(self, obj=None): + def objectContentsAreInClipboard(self, obj: Optional[Atspi.Accessible] = None) -> bool: obj = obj or cthulhu_state.locusOfFocus if not obj or AXObject.is_dead(obj): return False @@ -5215,13 +5198,13 @@ class Utilities: return obj and AXObject.get_name(obj) in contents - def clearCachedCommandState(self): + def clearCachedCommandState(self) -> bool: self._script.pointOfReference['undo'] = False self._script.pointOfReference['redo'] = False self._script.pointOfReference['paste'] = False self._script.pointOfReference['last-selection-message'] = '' - def handleUndoTextEvent(self, event): + def handleUndoTextEvent(self, event: Any) -> bool: if self.lastInputEventWasUndo(): if not self._script.pointOfReference.get('undo'): self._script.presentMessage(messages.UNDO) @@ -5238,7 +5221,7 @@ class Utilities: return False - def handleUndoLocusOfFocusChange(self): + def handleUndoLocusOfFocusChange(self) -> bool: if self._locusOfFocusIsTopLevelObject(): return False @@ -5256,7 +5239,7 @@ class Utilities: return False - def handlePasteLocusOfFocusChange(self): + def handlePasteLocusOfFocusChange(self) -> bool: if self._locusOfFocusIsTopLevelObject(): return False @@ -5269,7 +5252,7 @@ class Utilities: return False - def eventIsUserTriggered(self, event): + def eventIsUserTriggered(self, event: Any) -> bool: if not cthulhu_state.lastInputEvent: msg = "SCRIPT UTILITIES: Not user triggered: No last input event." debug.printMessage(debug.LEVEL_INFO, msg, True) @@ -5288,21 +5271,21 @@ class Utilities: return True - def isKeyGrabEvent(self, event): + def isKeyGrabEvent(self, event: Any) -> bool: """ Returns True if this event appears to be a side-effect of an X11 key grab. """ if not isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent): return False return cthulhu_state.lastInputEvent.didConsume() and not cthulhu_state.openingDialog - def presentFocusChangeReason(self): + def presentFocusChangeReason(self) -> bool: if self.handleUndoLocusOfFocusChange(): return True if self.handlePasteLocusOfFocusChange(): return True return False - def allItemsSelected(self, obj): + def allItemsSelected(self, obj: Atspi.Accessible) -> bool: if not AXObject.supports_selection(obj): return False @@ -5344,7 +5327,7 @@ class Utilities: return False - def handleContainerSelectionChange(self, obj): + def handleContainerSelectionChange(self, obj: Atspi.Accessible) -> bool: allAlreadySelected = self._script.pointOfReference.get('allItemsSelected') allCurrentlySelected = self.allItemsSelected(obj) if allAlreadySelected and allCurrentlySelected: @@ -5358,7 +5341,7 @@ class Utilities: return False - def handleTextSelectionChange(self, obj, speakMessage=True): + def handleTextSelectionChange(self, obj: Atspi.Accessible, speakMessage: Any = True) -> bool: # Note: This guesswork to figure out what actually changed with respect # to text selection will get eliminated once the new text-selection API # is added to ATK and implemented by the toolkits. (BGO 638378) @@ -5427,14 +5410,14 @@ class Utilities: return True - def _getCtrlShiftSelectionsStrings(self): + def _getCtrlShiftSelectionsStrings(self) -> Any: """Hacky and to-be-obsoleted method.""" return [messages.PARAGRAPH_SELECTED_DOWN, messages.PARAGRAPH_UNSELECTED_DOWN, messages.PARAGRAPH_SELECTED_UP, messages.PARAGRAPH_UNSELECTED_UP] - def _speakTextSelectionState(self, nSelections): + def _speakTextSelectionState(self, nSelections: Any) -> bool: """Hacky and to-be-obsoleted method.""" if cthulhu.cthulhuApp.settingsManager.getSetting('onlySpeakDisplayedText'): @@ -5498,7 +5481,7 @@ class Utilities: return True - def shouldInterruptForLocusOfFocusChange(self, oldLocusOfFocus, newLocusOfFocus, event=None): + def shouldInterruptForLocusOfFocusChange(self, oldLocusOfFocus: Any, newLocusOfFocus: Any, event: Any = None) -> bool: msg = "SCRIPT UTILITIES: Not interrupting for locusOfFocus change: " if event is None: msg += "event is None" @@ -5536,7 +5519,7 @@ class Utilities: return False return True - def stringsAreRedundant(self, str1, str2, threshold=0.5): + def stringsAreRedundant(self, str1: Any, str2: Any, threshold: Any = 0.5) -> bool: if not (str1 and str2): return False diff --git a/src/cthulhu/scripts/terminal/script_utilities.py b/src/cthulhu/scripts/terminal/script_utilities.py index f59ef3c..b61d9f7 100644 --- a/src/cthulhu/scripts/terminal/script_utilities.py +++ b/src/cthulhu/scripts/terminal/script_utilities.py @@ -36,6 +36,7 @@ from gi.repository import Atspi import re from cthulhu import debug +from cthulhu import cthulhu from cthulhu import keybindings from cthulhu import cthulhu_state from cthulhu import script_utilities diff --git a/src/cthulhu/signal_manager.py b/src/cthulhu/signal_manager.py index 51e61c2..762e3a6 100644 --- a/src/cthulhu/signal_manager.py +++ b/src/cthulhu/signal_manager.py @@ -25,19 +25,27 @@ import gi from gi.repository import GObject +from typing import Optional, Any, Callable, Tuple from cthulhu import resource_manager -class SignalManager(): - def __init__(self, app): - self.app = app - self.resourceManager = self.app.getResourceManager() +class SignalManager: + def __init__(self, app: Any) -> None: # app is CthulhuApp instance + self.app: Any = app + self.resourceManager: Any = self.app.getResourceManager() # ResourceManager - def registerSignal(self, signalName, signalFlag = GObject.SignalFlags.RUN_LAST, closure = GObject.TYPE_NONE, accumulator=(), contextName = None): + def registerSignal( + self, + signalName: str, + signalFlag: GObject.SignalFlags = GObject.SignalFlags.RUN_LAST, + closure: Any = GObject.TYPE_NONE, + accumulator: Tuple = (), + contextName: Optional[str] = None + ) -> bool: # register signal ok = False if not self.signalExist(signalName): - GObject.signal_new(signalName, self.app, signalFlag, closure,accumulator) + GObject.signal_new(signalName, self.app, signalFlag, closure, accumulator) ok = True resourceContext = self.resourceManager.getResourceContext(contextName) if resourceContext: @@ -45,9 +53,17 @@ class SignalManager(): resourceContext.addSignal(signalName, resourceEntry) return ok - def signalExist(self, signalName): + def signalExist(self, signalName: str) -> bool: return GObject.signal_lookup(signalName, self.app) != 0 - def connectSignal(self, signalName, function, profile, param = None, contextName = None): + + def connectSignal( + self, + signalName: str, + function: Callable, + profile: Any, + param: Optional[Any] = None, + contextName: Optional[str] = None + ) -> Optional[int]: signalID = None try: if self.signalExist(signalName): @@ -63,7 +79,8 @@ class SignalManager(): print(e) return signalID - def disconnectSignalByFunction(self, function, contextName = None): + + def disconnectSignalByFunction(self, function: Callable, contextName: Optional[str] = None) -> bool: ok = False try: self.app.disconnect_by_func(function) @@ -74,7 +91,8 @@ class SignalManager(): if resourceContext: resourceContext.removeSubscriptionByFunction(function) return ok - def emitSignal(self, signalName, *args): + + def emitSignal(self, signalName: str, *args: Any) -> None: # emit a signal with optional arguments try: self.app.emit(signalName, *args) diff --git a/src/cthulhu/sound.py b/src/cthulhu/sound.py index a427d10..3114799 100644 --- a/src/cthulhu/sound.py +++ b/src/cthulhu/sound.py @@ -33,12 +33,13 @@ __license__ = "LGPL" import gi from gi.repository import GLib +from typing import Optional, Any try: gi.require_version('Gst', '1.0') from gi.repository import Gst except Exception: - _gstreamerAvailable = False + _gstreamerAvailable: bool = False else: _gstreamerAvailable, args = Gst.init_check(None) @@ -48,12 +49,12 @@ from .sound_generator import Icon, Tone class Player: """Plays Icons and Tones.""" - def __init__(self): - self._initialized = False - self._source = None - self._sink = None - self._player = None - self._pipeline = None + def __init__(self) -> None: + self._initialized: bool = False + self._source: Optional[Any] = None # Optional[Gst.Element] + self._sink: Optional[Any] = None # Optional[Gst.Element] + self._player: Optional[Any] = None # Optional[Gst.Element] + self._pipeline: Optional[Any] = None # Optional[Gst.Pipeline] if not _gstreamerAvailable: msg = 'SOUND ERROR: Gstreamer is not available' @@ -62,7 +63,7 @@ class Player: self.init() - def _onPlayerMessage(self, bus, message): + def _onPlayerMessage(self, bus: Any, message: Any) -> None: # bus: Gst.Bus, message: Gst.Message if message.type == Gst.MessageType.EOS: self._player.set_state(Gst.State.NULL) elif message.type == Gst.MessageType.ERROR: @@ -71,7 +72,7 @@ class Player: msg = f'SOUND ERROR: {error}' debug.printMessage(debug.LEVEL_INFO, msg, True) - def _onPipelineMessage(self, bus, message): + def _onPipelineMessage(self, bus: Any, message: Any) -> None: # bus: Gst.Bus, message: Gst.Message if message.type == Gst.MessageType.EOS: self._pipeline.set_state(Gst.State.NULL) elif message.type == Gst.MessageType.ERROR: @@ -80,11 +81,11 @@ class Player: msg = f'SOUND ERROR: {error}' debug.printMessage(debug.LEVEL_INFO, msg, True) - def _onTimeout(self, element): + def _onTimeout(self, element: Any) -> bool: # element: Gst.Element element.set_state(Gst.State.NULL) return False - def _playIcon(self, icon, interrupt=True): + def _playIcon(self, icon: Icon, interrupt: bool = True) -> None: """Plays a sound icon, interrupting the current play first unless specified.""" if interrupt: @@ -93,7 +94,7 @@ class Player: self._player.set_property('uri', f'file://{icon.path}') self._player.set_state(Gst.State.PLAYING) - def _playIconAndWait(self, icon, interrupt=True, timeout_seconds=10): + def _playIconAndWait(self, icon: Icon, interrupt: bool = True, timeout_seconds: Optional[int] = 10) -> bool: """Plays a sound icon and waits for completion.""" if interrupt: @@ -123,7 +124,7 @@ class Player: self._player.set_state(Gst.State.NULL) return message is not None and message.type == Gst.MessageType.EOS - def _playTone(self, tone, interrupt=True): + def _playTone(self, tone: Tone, interrupt: bool = True) -> None: """Plays a tone, interrupting the current play first unless specified.""" if interrupt: @@ -136,7 +137,7 @@ class Player: duration = int(1000 * tone.duration) GLib.timeout_add(duration, self._onTimeout, self._pipeline) - def init(self): + def init(self) -> None: """(Re)Initializes the Player.""" if self._initialized: