# Orca # # Copyright 2005-2009 Sun Microsystems Inc. # Copyright 2010 Orca Team. # Copyright 2014-2025 Igalia, S.L. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # pylint: disable=too-many-boolean-expressions # pylint: disable=too-many-branches # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-lines # pylint: disable=too-many-locals # pylint: disable=too-many-public-methods # pylint: disable=too-many-return-statements # pylint: disable=too-many-statements # pylint: disable=wrong-import-position """Provides support for accessing user-agent-agnostic web-content.""" # This has to be the first non-docstring line in the module to make linters happy. from __future__ import annotations __id__ = "$Id$" __version__ = "$Revision$" __date__ = "$Date$" __copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." \ "Copyright (c) 2010 Orca Team." \ "Copyright (c) 2014-2025 Igalia, S.L." __license__ = "LGPL" from typing import TYPE_CHECKING import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk from orca import braille from orca import caret_navigator from orca import cmdnames from orca import keybindings from orca import debug from orca import focus_manager from orca import guilabels from orca import input_event from orca import input_event_manager from orca import label_inference from orca import liveregions from orca import messages from orca import settings from orca import settings_manager from orca import speech from orca.scripts import default from orca.ax_component import AXComponent from orca.ax_document import AXDocument from orca.ax_event_synthesizer import AXEventSynthesizer from orca.ax_object import AXObject from orca.ax_table import AXTable from orca.ax_text import AXText from orca.ax_utilities import AXUtilities from orca.ax_utilities_event import TextEventReason from orca.structural_navigator import NavigationMode from .bookmarks import Bookmarks from .braille_generator import BrailleGenerator from .speech_generator import SpeechGenerator from .script_utilities import Utilities if TYPE_CHECKING: gi.require_version("Atspi", "2.0") from gi.repository import Atspi class Script(default.Script): """Provides support for accessing user-agent-agnostic web-content.""" # Type annotations to override the base class types utilities: Utilities caret_navigator: caret_navigator.CaretNavigator live_region_manager: liveregions.LiveRegionManager bookmarks: Bookmarks def __init__(self, app: Atspi.Accessible) -> None: super().__init__(app) self._default_sn_mode = NavigationMode.DOCUMENT self._default_caret_navigation_enabled: bool = True self._loading_content = False self._made_find_announcement = False self._last_mouse_button_context = None, -1 self._in_focus_mode = False self._focus_mode_is_sticky = False self._browse_mode_is_sticky = False if settings_manager.get_manager().get_setting("sayAllOnLoad") is None: settings_manager.get_manager().set_setting("sayAllOnLoad", True) if settings_manager.get_manager().get_setting("pageSummaryOnLoad") is None: settings_manager.get_manager().set_setting("pageSummaryOnLoad", True) self._changed_lines_only_check_button: Gtk.CheckButton | None = None self._control_caret_navigation_check_button: Gtk.CheckButton | None = None self._minimum_find_length_adjustment: Gtk.Adjustment | None = None self._minimum_find_length_label: Gtk.Label | None = None self._minimum_find_length_spin_button: Gtk.SpinButton | None = None self._page_summary_on_load_check_button: Gtk.CheckButton | None = None self._say_all_on_load_check_button: Gtk.CheckButton | None = None self._skip_blank_cells_check_button: Gtk.CheckButton | None = None self._speak_cell_coordinates_check_button: Gtk.CheckButton | None = None self._speak_cell_headers_check_button: Gtk.CheckButton | None = None self._speak_cell_span_check_button: Gtk.CheckButton | None = None self._speak_results_during_find_check_button: Gtk.CheckButton | None = None self._structural_navigation_check_button: Gtk.CheckButton | None = None self._auto_focus_mode_struct_nav_check_button: Gtk.CheckButton | None = None self._auto_focus_mode_caret_nav_check_button: Gtk.CheckButton | None = None self._auto_focus_mode_native_nav_check_button: Gtk.CheckButton | None = None self._layout_mode_check_button: Gtk.CheckButton | None = None def activate(self) -> None: """Called when this script is activated.""" tokens = ["WEB: Activating script for", self.app] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus = focus_manager.get_manager().get_locus_of_focus() in_app = AXUtilities.get_application(focus) == self.app in_doc = self.utilities.in_document_content(focus) suspend = not (in_doc and in_app) reason = f"script activation, not in document content in this app: {suspend}" self.get_caret_navigator().suspend_commands(self, suspend, reason) self.get_structural_navigator().suspend_commands(self, suspend, reason) self.live_region_manager.suspend_commands(self, suspend, reason) self.get_table_navigator().suspend_commands(self, suspend, reason) super().activate() def deactivate(self) -> None: """Called when this script is deactivated.""" self._loading_content = False self._made_find_announcement = False self._last_mouse_button_context = None, -1 self.utilities.clear_cached_objects() reason = "script deactivation" self.get_caret_navigator().suspend_commands(self, False, reason) self.get_structural_navigator().suspend_commands(self, False, reason) self.live_region_manager.suspend_commands(self, False, reason) self.get_table_navigator().suspend_commands(self, False, reason) super().deactivate() def get_app_key_bindings(self) -> keybindings.KeyBindings: """Returns the application-specific keybindings for this script.""" key_bindings = keybindings.KeyBindings() layout = settings_manager.get_manager().get_setting("keyboardLayout") is_desktop = layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP live_region_bindings = self.live_region_manager.get_bindings( refresh=True, is_desktop=is_desktop) for key_binding in live_region_bindings.key_bindings: key_bindings.add(key_binding) key_bindings.add( keybindings.KeyBinding( "a", keybindings.DEFAULT_MODIFIER_MASK, keybindings.ORCA_MODIFIER_MASK, self.input_event_handlers["togglePresentationModeHandler"])) key_bindings.add( keybindings.KeyBinding( "a", keybindings.DEFAULT_MODIFIER_MASK, keybindings.ORCA_MODIFIER_MASK, self.input_event_handlers["enableStickyFocusModeHandler"], 2)) key_bindings.add( keybindings.KeyBinding( "a", keybindings.DEFAULT_MODIFIER_MASK, keybindings.ORCA_MODIFIER_MASK, self.input_event_handlers["enableStickyBrowseModeHandler"], 3)) key_bindings.add( keybindings.KeyBinding( "", keybindings.DEFAULT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, self.input_event_handlers["toggleLayoutModeHandler"])) return key_bindings def setup_input_event_handlers(self) -> None: """Defines the input event handlers for this script.""" super().setup_input_event_handlers() self.input_event_handlers.update(self.live_region_manager.get_handlers(True)) self.input_event_handlers["panBrailleLeftHandler"] = \ input_event.InputEventHandler( Script.pan_braille_left, cmdnames.PAN_BRAILLE_LEFT, False) # Do not enable learn mode for this action self.input_event_handlers["panBrailleRightHandler"] = \ input_event.InputEventHandler( Script.pan_braille_right, cmdnames.PAN_BRAILLE_RIGHT, False) # Do not enable learn mode for this action self.input_event_handlers["togglePresentationModeHandler"] = \ input_event.InputEventHandler( Script.toggle_presentation_mode, cmdnames.TOGGLE_PRESENTATION_MODE) self.input_event_handlers["enableStickyFocusModeHandler"] = \ input_event.InputEventHandler( Script.enable_sticky_focus_mode, cmdnames.SET_FOCUS_MODE_STICKY) self.input_event_handlers["enableStickyBrowseModeHandler"] = \ input_event.InputEventHandler( Script.enable_sticky_browse_mode, cmdnames.SET_BROWSE_MODE_STICKY) self.input_event_handlers["toggleLayoutModeHandler"] = \ input_event.InputEventHandler( Script.toggle_layout_mode, cmdnames.TOGGLE_LAYOUT_MODE) def get_bookmarks(self) -> Bookmarks: """Returns the "bookmarks" class for this script.""" try: return self.bookmarks except AttributeError: self.bookmarks = Bookmarks(self) return self.bookmarks def get_braille_generator(self) -> BrailleGenerator: """Returns the braille generator for this script.""" return BrailleGenerator(self) def get_label_inference(self) -> label_inference.LabelInference | None: """Returns the label inference functionality for this script.""" return label_inference.LabelInference(self) def get_live_region_manager(self) -> liveregions.LiveRegionManager: """Returns the live region support for this script.""" return liveregions.LiveRegionManager(self) def get_speech_generator(self) -> SpeechGenerator: """Returns the speech generator for this script.""" return SpeechGenerator(self) def get_utilities(self) -> Utilities: """Returns the utilities for this script.""" return Utilities(self) def get_app_preferences_gui(self) -> Gtk.Grid: """Return a GtkGrid containing app-unique configuration items.""" grid = Gtk.Grid() grid.set_border_width(12) general_frame = Gtk.Frame() grid.attach(general_frame, 0, 0, 1, 1) label = Gtk.Label(label=f"{guilabels.PAGE_NAVIGATION}") label.set_use_markup(True) general_frame.set_label_widget(label) general_alignment = Gtk.Alignment.new(0.5, 0.5, 1, 1) general_alignment.set_padding(0, 0, 12, 0) general_frame.add(general_alignment) general_grid = Gtk.Grid() general_alignment.add(general_grid) label = guilabels.USE_CARET_NAVIGATION value = self.get_caret_navigator().get_is_enabled() self._control_caret_navigation_check_button = \ Gtk.CheckButton.new_with_mnemonic(label) self._control_caret_navigation_check_button.set_active(value) general_grid.attach(self._control_caret_navigation_check_button, 0, 0, 1, 1) label = guilabels.AUTO_FOCUS_MODE_CARET_NAV value = self.get_caret_navigator().get_triggers_focus_mode() self._auto_focus_mode_caret_nav_check_button = Gtk.CheckButton.new_with_mnemonic(label) self._auto_focus_mode_caret_nav_check_button.set_active(value) general_grid.attach(self._auto_focus_mode_caret_nav_check_button, 0, 1, 1, 1) label = guilabels.USE_STRUCTURAL_NAVIGATION value = self.get_structural_navigator().get_is_enabled() self._structural_navigation_check_button = \ Gtk.CheckButton.new_with_mnemonic(label) self._structural_navigation_check_button.set_active(value) general_grid.attach(self._structural_navigation_check_button, 0, 2, 1, 1) label = guilabels.AUTO_FOCUS_MODE_STRUCT_NAV value = self.get_structural_navigator().get_triggers_focus_mode() self._auto_focus_mode_struct_nav_check_button = Gtk.CheckButton.new_with_mnemonic(label) self._auto_focus_mode_struct_nav_check_button.set_active(value) general_grid.attach(self._auto_focus_mode_struct_nav_check_button, 0, 3, 1, 1) label = guilabels.AUTO_FOCUS_MODE_NATIVE_NAV value = settings_manager.get_manager().get_setting("nativeNavTriggersFocusMode") self._auto_focus_mode_native_nav_check_button = Gtk.CheckButton.new_with_mnemonic(label) self._auto_focus_mode_native_nav_check_button.set_active(value) general_grid.attach(self._auto_focus_mode_native_nav_check_button, 0, 4, 1, 1) label = guilabels.READ_PAGE_UPON_LOAD value = settings_manager.get_manager().get_setting("sayAllOnLoad") self._say_all_on_load_check_button = Gtk.CheckButton.new_with_mnemonic(label) self._say_all_on_load_check_button.set_active(value) general_grid.attach(self._say_all_on_load_check_button, 0, 5, 1, 1) label = guilabels.PAGE_SUMMARY_UPON_LOAD value = settings_manager.get_manager().get_setting("pageSummaryOnLoad") self._page_summary_on_load_check_button = Gtk.CheckButton.new_with_mnemonic(label) self._page_summary_on_load_check_button.set_active(value) general_grid.attach(self._page_summary_on_load_check_button, 0, 6, 1, 1) label = guilabels.CONTENT_LAYOUT_MODE value = settings_manager.get_manager().get_setting("layoutMode") self._layout_mode_check_button = Gtk.CheckButton.new_with_mnemonic(label) self._layout_mode_check_button.set_active(value) general_grid.attach(self._layout_mode_check_button, 0, 7, 1, 1) table_frame = Gtk.Frame() grid.attach(table_frame, 0, 1, 1, 1) label = Gtk.Label(label=f"{guilabels.TABLE_NAVIGATION}") label.set_use_markup(True) table_frame.set_label_widget(label) table_alignment = Gtk.Alignment.new(0.5, 0.5, 1, 1) table_alignment.set_padding(0, 0, 12, 0) table_frame.add(table_alignment) table_grid = Gtk.Grid() table_alignment.add(table_grid) label = guilabels.TABLE_SPEAK_CELL_COORDINATES value = settings_manager.get_manager().get_setting("speakCellCoordinates") self._speak_cell_coordinates_check_button = \ Gtk.CheckButton.new_with_mnemonic(label) self._speak_cell_coordinates_check_button.set_active(value) table_grid.attach(self._speak_cell_coordinates_check_button, 0, 0, 1, 1) label = guilabels.TABLE_SPEAK_CELL_SPANS value = settings_manager.get_manager().get_setting("speakCellSpan") self._speak_cell_span_check_button = \ Gtk.CheckButton.new_with_mnemonic(label) self._speak_cell_span_check_button.set_active(value) table_grid.attach(self._speak_cell_span_check_button, 0, 1, 1, 1) label = guilabels.TABLE_ANNOUNCE_CELL_HEADER value = settings_manager.get_manager().get_setting("speakCellHeaders") self._speak_cell_headers_check_button = \ Gtk.CheckButton.new_with_mnemonic(label) self._speak_cell_headers_check_button.set_active(value) table_grid.attach(self._speak_cell_headers_check_button, 0, 2, 1, 1) label = guilabels.TABLE_SKIP_BLANK_CELLS value = self.get_table_navigator().get_skip_blank_cells() self._skip_blank_cells_check_button = \ Gtk.CheckButton.new_with_mnemonic(label) self._skip_blank_cells_check_button.set_active(value) table_grid.attach(self._skip_blank_cells_check_button, 0, 3, 1, 1) find_frame = Gtk.Frame() grid.attach(find_frame, 0, 2, 1, 1) label = Gtk.Label(label=f"{guilabels.FIND_OPTIONS}") label.set_use_markup(True) find_frame.set_label_widget(label) find_alignment = Gtk.Alignment.new(0.5, 0.5, 1, 1) find_alignment.set_padding(0, 0, 12, 0) find_frame.add(find_alignment) find_grid = Gtk.Grid() find_alignment.add(find_grid) verbosity = settings_manager.get_manager().get_setting("findResultsVerbosity") label = guilabels.FIND_SPEAK_RESULTS value = verbosity != settings.FIND_SPEAK_NONE self._speak_results_during_find_check_button = \ Gtk.CheckButton.new_with_mnemonic(label) self._speak_results_during_find_check_button.set_active(value) find_grid.attach(self._speak_results_during_find_check_button, 0, 0, 1, 1) label = guilabels.FIND_ONLY_SPEAK_CHANGED_LINES value = verbosity == settings.FIND_SPEAK_IF_LINE_CHANGED self._changed_lines_only_check_button = \ Gtk.CheckButton.new_with_mnemonic(label) self._changed_lines_only_check_button.set_active(value) find_grid.attach(self._changed_lines_only_check_button, 0, 1, 1, 1) hgrid = Gtk.Grid() find_grid.attach(hgrid, 0, 2, 1, 1) self._minimum_find_length_label = \ Gtk.Label(label=guilabels.FIND_MINIMUM_MATCH_LENGTH) self._minimum_find_length_label.set_alignment(0, 0.5) hgrid.attach(self._minimum_find_length_label, 0, 0, 1, 1) self._minimum_find_length_adjustment = \ Gtk.Adjustment(settings_manager.get_manager().get_setting( "findResultsMinimumLength"), 0, 20, 1) self._minimum_find_length_spin_button = Gtk.SpinButton() self._minimum_find_length_spin_button.set_adjustment( self._minimum_find_length_adjustment) hgrid.attach(self._minimum_find_length_spin_button, 1, 0, 1, 1) self._minimum_find_length_label.set_mnemonic_widget( self._minimum_find_length_spin_button) grid.show_all() return grid def get_preferences_from_gui(self) -> dict[str, bool | int]: """Returns a dictionary with the app-specific preferences.""" assert self._speak_results_during_find_check_button is not None assert self._changed_lines_only_check_button is not None assert self._minimum_find_length_spin_button is not None assert self._say_all_on_load_check_button is not None assert self._page_summary_on_load_check_button is not None assert self._structural_navigation_check_button is not None assert self._auto_focus_mode_struct_nav_check_button is not None assert self._control_caret_navigation_check_button is not None assert self._auto_focus_mode_caret_nav_check_button is not None assert self._auto_focus_mode_native_nav_check_button is not None assert self._speak_cell_coordinates_check_button is not None assert self._layout_mode_check_button is not None assert self._speak_cell_span_check_button is not None assert self._speak_cell_headers_check_button is not None assert self._skip_blank_cells_check_button is not None if not self._speak_results_during_find_check_button.get_active(): verbosity = settings.FIND_SPEAK_NONE elif self._changed_lines_only_check_button.get_active(): verbosity = settings.FIND_SPEAK_IF_LINE_CHANGED else: verbosity = settings.FIND_SPEAK_ALL return { "findResultsVerbosity": verbosity, "findResultsMinimumLength": self._minimum_find_length_spin_button.get_value(), "sayAllOnLoad": self._say_all_on_load_check_button.get_active(), "pageSummaryOnLoad": self._page_summary_on_load_check_button.get_active(), "structuralNavigationEnabled": self._structural_navigation_check_button.get_active(), "structNavTriggersFocusMode": self._auto_focus_mode_struct_nav_check_button.get_active(), "caretNavigationEnabled": self._control_caret_navigation_check_button.get_active(), "caretNavTriggersFocusMode": self._auto_focus_mode_caret_nav_check_button.get_active(), "nativeNavTriggersFocusMode": self._auto_focus_mode_native_nav_check_button.get_active(), "speakCellCoordinates": self._speak_cell_coordinates_check_button.get_active(), "layoutMode": self._layout_mode_check_button.get_active(), "speakCellSpan": self._speak_cell_span_check_button.get_active(), "speakCellHeaders": self._speak_cell_headers_check_button.get_active(), "skipBlankCells": self._skip_blank_cells_check_button.get_active() } def _present_find_results(self, obj: Atspi.Accessible, offset: int) -> None: """Updates the context and presents the find results if appropriate.""" document = self.utilities.get_document_for_object(obj) if not document: return start = AXText.get_selection_start_offset(obj) if start < 0: return offset = max(offset, start) context = self.utilities.get_caret_context(document) self.utilities.set_caret_context(obj, offset, document=document) end = AXText.get_selection_end_offset(obj) if end - start < settings_manager.get_manager().get_setting("findResultsMinimumLength"): return verbosity = settings_manager.get_manager().get_setting("findResultsVerbosity") if verbosity == settings.FIND_SPEAK_NONE: return if self._made_find_announcement \ and verbosity == settings.FIND_SPEAK_IF_LINE_CHANGED \ and self.utilities.contexts_are_on_same_line(context, (obj, offset)): return contents = self.utilities.get_line_contents_at_offset(obj, offset) self.speak_contents(contents) self.update_braille(obj) results_count = self.utilities.get_find_results_count() if results_count: self.present_message(results_count) self._made_find_announcement = True def in_layout_mode(self) -> bool: """ Returns True if we're in layout mode.""" return settings_manager.get_manager().get_setting("layoutMode") def in_focus_mode(self) -> bool: """ Returns True if we're in focus mode.""" return self._in_focus_mode def focus_mode_is_sticky(self) -> bool: """Returns True if we're in 'sticky' focus mode.""" return self._focus_mode_is_sticky def browse_mode_is_sticky(self) -> bool: """Returns True if we're in 'sticky' browse mode.""" return self._browse_mode_is_sticky def use_focus_mode( self, obj: Atspi.Accessible, prev_obj: Atspi.Accessible | None = None ) -> bool: """Returns True if we should use focus mode in obj.""" if self._focus_mode_is_sticky: msg = "WEB: Using focus mode because focus mode is sticky" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self._browse_mode_is_sticky: msg = "WEB: Not using focus mode because browse mode is sticky" debug.print_message(debug.LEVEL_INFO, msg, True) return False if focus_manager.get_manager().in_say_all(): msg = "WEB: Not using focus mode because we're in SayAll." debug.print_message(debug.LEVEL_INFO, msg, True) return False if self.get_structural_navigator().last_command_prevents_focus_mode(): msg = "WEB: Not using focus mode due to struct nav settings" debug.print_message(debug.LEVEL_INFO, msg, True) return False if self.get_table_navigator().last_input_event_was_navigation_command(): msg = "WEB: Not using focus mode because last command was table navigation" debug.print_message(debug.LEVEL_INFO, msg, True) return False if prev_obj and AXObject.is_dead(prev_obj): prev_obj = None if self.get_caret_navigator().last_command_prevents_focus_mode() \ and AXObject.find_ancestor_inclusive(prev_obj, AXUtilities.is_tool_tip) is None: msg = "WEB: Not using focus mode due to caret nav settings" debug.print_message(debug.LEVEL_INFO, msg, True) return False if not settings_manager.get_manager().get_setting("nativeNavTriggersFocusMode"): struct_nav = self.get_structural_navigator().last_input_event_was_navigation_command() caret_nav = self.get_caret_navigator().last_input_event_was_navigation_command() if not (struct_nav or caret_nav): msg = "WEB: Not changing focus/browse mode due to native nav settings" debug.print_message(debug.LEVEL_INFO, msg, True) return self._in_focus_mode if self.utilities.is_focus_mode_widget(obj): tokens = ["WEB: Using focus mode because", obj, "is a focus mode widget"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True do_not_toggle = AXUtilities.is_link(obj) or AXUtilities.is_radio_button(obj) if self._in_focus_mode and do_not_toggle \ and input_event_manager.get_manager().last_event_was_unmodified_arrow(): tokens = ["WEB: Staying in focus mode due to arrowing in role of", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True was_in_app = AXObject.find_ancestor(prev_obj, AXUtilities.is_embedded) is_in_app = AXObject.find_ancestor(obj, AXUtilities.is_embedded) if not was_in_app and is_in_app: msg = "WEB: Using focus mode because we just entered a web application" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self._in_focus_mode and is_in_app: if self.utilities.force_browse_mode_for_web_app_descendant(obj): tokens = ["WEB: Forcing browse mode for web app descendant", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False msg = "WEB: Staying in focus mode because we're inside a web application" debug.print_message(debug.LEVEL_INFO, msg, True) return True tokens = ["WEB: Not using focus mode for", obj, "due to lack of cause"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False def say_character(self, obj: Atspi.Accessible) -> None: """Speaks the character at the current caret position.""" tokens = ["WEB: Say character for", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not self.utilities.in_document_content(obj): msg = "WEB: Object is not in document content." debug.print_message(debug.LEVEL_INFO, msg, True) super().say_character(obj) return document = self.utilities.get_top_level_document_for_object(obj) obj, offset = self.utilities.get_caret_context(document=document) tokens = ["WEB: Adjusted object and offset for say character to", obj, offset] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not obj: return contents: list[tuple[Atspi.Accessible, int, int, str]] | None = None if self.utilities.treat_as_end_of_line(obj, offset) and AXObject.supports_text(obj): char = AXText.get_character_at_offset(obj, offset)[0] if char == "\ufffc": char = "" contents = [(obj, offset, offset + 1, char)] else: contents = self.utilities.get_character_contents_at_offset(obj, offset) if not contents: return obj, start, _end, string = contents[0] if start > 0 and string == "\n": if settings_manager.get_manager().get_setting("speakBlankLines"): self.speak_message(messages.BLANK, interrupt=False) return if string: self.speak_misspelled_indicator(obj, start) self.speak_character(string) else: self.speak_contents(contents) self.point_of_reference["lastTextUnitSpoken"] = "char" def say_word(self, obj: Atspi.Accessible) -> None: """Speaks the word at the current caret position.""" tokens = ["WEB: Say word for", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not self.utilities.in_document_content(obj): msg = "WEB: Object is not in document content." debug.print_message(debug.LEVEL_INFO, msg, True) super().say_word(obj) return document = self.utilities.get_top_level_document_for_object(obj) obj, offset = self.utilities.get_caret_context(document=document) if input_event_manager.get_manager().last_event_was_right(): offset -= 1 tokens = ["WEB: Adjusted object and offset for say word to", obj, offset] debug.print_tokens(debug.LEVEL_INFO, tokens, True) word_contents = self.utilities.get_word_contents_at_offset(obj, offset, use_cache=True) text_obj, start_offset, _end_offset, _word = word_contents[0] self.speak_misspelled_indicator(text_obj, start_offset) # TODO - JD: Clean up the focused + alreadyFocused mess which by side effect is causing # the content of some objects (e.g. table cells) to not be generated. self.speak_contents(word_contents, alreadyFocused=AXUtilities.is_text_input(text_obj)) self.point_of_reference["lastTextUnitSpoken"] = "word" def say_line(self, obj: Atspi.Accessible, offset: int | None = None) -> None: """Speaks the line at the current caret position.""" tokens = ["WEB: Say line for", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not self.utilities.in_document_content(obj): msg = "WEB: Object is not in document content." debug.print_message(debug.LEVEL_INFO, msg, True) super().say_line(obj) return # TODO - JD: We're making an exception here because the default script's say_line() # handles verbalized punctuation, indentation, repeats, etc. That adjustment belongs # in the generators, but that's another potentially non-trivial change. if AXUtilities.is_editable(obj) and "\ufffc" not in AXText.get_line_at_offset(obj)[0]: msg = "WEB: Object is editable and line has no EOCs." debug.print_message(debug.LEVEL_INFO, msg, True) if not self._in_focus_mode: self.utilities.set_caret_position(obj, 0) super().say_line(obj) return document = self.utilities.get_top_level_document_for_object(obj) prior_context = self.utilities.get_prior_context(document=document) if prior_context is not None: prior_obj, _prior_offset = prior_context else: prior_obj = None if offset is None: obj, offset = self.utilities.get_caret_context(document) tokens = ["WEB: Adjusted object and offset for say line to", obj, offset] debug.print_tokens(debug.LEVEL_INFO, tokens, True) contents = self.utilities.get_line_contents_at_offset(obj, offset, use_cache=True) if contents and contents[0] and not self._in_focus_mode: self.utilities.set_caret_position(contents[0][0], contents[0][1]) self.speak_contents(contents, priorObj=prior_obj) self.point_of_reference["lastTextUnitSpoken"] = "line" def present_object(self, obj: Atspi.Accessible, **args) -> None: if obj is None: return if not self.utilities.in_document_content(obj) or AXUtilities.is_document(obj): super().present_object(obj, **args) return mode, _obj = focus_manager.get_manager().get_active_mode_and_object_of_interest() if mode in [focus_manager.OBJECT_NAVIGATOR, focus_manager.MOUSE_REVIEW]: super().present_object(obj, **args) return if AXUtilities.is_status_bar(obj) or AXUtilities.is_alert(obj): if not self._in_focus_mode: self.utilities.set_caret_position(obj, 0) super().present_object(obj, **args) return prior_obj = args.get("priorObj") if self.get_caret_navigator().last_input_event_was_navigation_command() \ or self.get_structural_navigator().last_input_event_was_navigation_command() \ or self.get_table_navigator().last_input_event_was_navigation_command() \ or args.get("includeContext") or AXTable.get_table(obj): prior_context = self.utilities.get_prior_context() if prior_context is not None: prior_obj, _prior_offset = prior_context args["priorObj"] = prior_obj # Objects might be destroyed as a consequence of scrolling, such as in an infinite scroll # list. Therefore, store its name and role beforehand. Objects in the process of being # destroyed typically lose their name even if they lack the defunct state. If the name of # the object is different after scrolling, we'll try to find a child with the same name and # role. document = self.utilities.get_document_for_object(obj) name = AXObject.get_name(obj) role = AXObject.get_role(obj) AXEventSynthesizer.scroll_to_center(obj, start_offset=0) if (name and AXObject.get_name(obj) != name) or AXObject.get_index_in_parent(obj) < 0: tokens = ["WEB:", obj, "believed to be destroyed after scroll."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) replicant = AXObject.find_descendant( document, lambda x: AXObject.get_name(x) == name and AXObject.get_role(obj) == role) if replicant: obj = replicant tokens = ["WEB: Replacing destroyed object with", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) # Reasons we don't want to dive deep into the object include: # 1. Editors like VSCode use the entry role for the code editor. # 2. Giant nested lists. if AXUtilities.is_entry(obj) or AXUtilities.is_list_item(obj): if not self._in_focus_mode: self.utilities.set_caret_position(obj, 0) super().present_object(obj, **args) return interrupt = args.get("interrupt", False) tokens = ["WEB: Presenting object", obj, ". Interrupt:", interrupt] debug.print_tokens(debug.LEVEL_INFO, tokens, True) # We shouldn't use cache in this method, because if the last thing we presented # included this object and offset (e.g. a Say All or Mouse Review), we're in # danger of presented irrelevant context. offset = args.get("offset", 0) contents = self.utilities.get_object_contents_at_offset(obj, offset, use_cache=False) if contents and contents[0] and not self._in_focus_mode: self.utilities.set_caret_position(contents[0][0], contents[0][1]) self.display_contents(contents) self.speak_contents(contents, **args) def _update_braille_caret_position(self, obj: Atspi.Accessible) -> None: """Try to reposition the cursor without having to do a full update.""" if "\ufffc" in AXText.get_all_text(obj): self.update_braille(obj) return super()._update_braille_caret_position(obj) def update_braille(self, obj: Atspi.Accessible, **args) -> None: """Updates the braille display to show the given object.""" tokens = ["WEB: updating braille for", obj, args] debug.print_tokens(debug.LEVEL_INFO, tokens, True, True) if not settings_manager.get_manager().get_setting("enableBraille") \ and not settings_manager.get_manager().get_setting("enableBrailleMonitor"): debug.print_message(debug.LEVEL_INFO, "BRAILLE: disabled", True) return if self._in_focus_mode and "\ufffc" not in AXText.get_all_text(obj): tokens = ["WEB: updating braille in focus mode", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) super().update_braille(obj, **args) return document = args.get("documentFrame", self.utilities.get_top_level_document_for_object(obj)) if not document: tokens = ["WEB: updating braille for non-document object", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) super().update_braille(obj, **args) return is_content_editable = self.utilities.is_content_editable_with_embedded_objects(obj) if not self.get_caret_navigator().last_input_event_was_navigation_command() \ and not self.get_structural_navigator().last_input_event_was_navigation_command() \ and not self.get_table_navigator().last_input_event_was_navigation_command() \ and not is_content_editable \ and not AXDocument.is_plain_text(document) \ and not input_event_manager.get_manager().last_event_was_caret_selection(): tokens = ["WEB: updating braille for unhandled navigation type", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) super().update_braille(obj, **args) return # TODO - JD: Getting the caret context can, by side effect, update it. This in turn # can prevent us from presenting table column headers when braille is enabled because # we think they are not "new." Commit bd877203f0 addressed that, but we need to stop # such side effects from happening in the first place. offset = args.get("offset") if offset is None: obj, offset = self.utilities.get_caret_context( document, get_replicant=True) if offset > 0 and is_content_editable \ and self.utilities.treat_as_text_object(obj): offset = min(offset, AXText.get_character_count(obj)) contents = self.utilities.get_line_contents_at_offset(obj, offset) self.display_contents(contents, documentFrame=document) def pan_braille_left( self, event: input_event.InputEvent | None = None, pan_amount: int = 0 ) -> bool: """Pans braille to the left.""" if self.get_flat_review_presenter().is_active() \ or not self.utilities.in_document_content() or not braille.beginningIsShowing: return super().pan_braille_left(event, pan_amount) contents = self.utilities.get_previous_line_contents() if not contents: return False obj, start, _end, _string = contents[0] self.utilities.set_caret_position(obj, start) self.update_braille(obj) # Hack: When panning to the left in a document, we want to start at # the right/bottom of each new object. For now, we'll pan there. # When time permits, we'll give our braille code some smarts. while braille.panRight(0): pass braille.refresh(False) return True def pan_braille_right( self, event: input_event.InputEvent | None = None, pan_amount: int = 0 ) -> bool: """Pans braille to the right.""" if self.get_flat_review_presenter().is_active() \ or not self.utilities.in_document_content() or not braille.endIsShowing: return super().pan_braille_right(event, pan_amount) contents = self.utilities.get_next_line_contents() if not contents: return False obj, start, _end, _string = contents[0] self.utilities.set_caret_position(obj, start) self.update_braille(obj) # Hack: When panning to the right in a document, we want to start at # the left/top of each new object. For now, we'll pan there. When time # permits, we'll give our braille code some smarts. while braille.panLeft(0): pass braille.refresh(False) return True def enable_sticky_browse_mode( self, _event: input_event.InputEvent | None = None, force_message: bool = False ) -> bool: """Enables sticky browse mode.""" if not self._browse_mode_is_sticky or force_message: self.present_message(messages.MODE_BROWSE_IS_STICKY) self._in_focus_mode = False self._focus_mode_is_sticky = False self._browse_mode_is_sticky = True reason = "enable sticky browse mode" self.get_caret_navigator().suspend_commands(self, self._in_focus_mode, reason) self.get_structural_navigator().suspend_commands(self, self._in_focus_mode, reason) self.live_region_manager.suspend_commands(self, self._in_focus_mode, reason) self.get_table_navigator().suspend_commands(self, self._in_focus_mode, reason) return True def enable_sticky_focus_mode( self, _event: input_event.InputEvent | None = None, force_message: bool = False ) -> bool: """Enables sticky focus mode.""" if not self._focus_mode_is_sticky or force_message: self.present_message(messages.MODE_FOCUS_IS_STICKY) self._in_focus_mode = True self._focus_mode_is_sticky = True self._browse_mode_is_sticky = False reason = "enable sticky focus mode" self.get_caret_navigator().suspend_commands(self, self._in_focus_mode, reason) self.get_structural_navigator().suspend_commands(self, self._in_focus_mode, reason) self.live_region_manager.suspend_commands(self, self._in_focus_mode, reason) self.get_table_navigator().suspend_commands(self, self._in_focus_mode, reason) return True def toggle_layout_mode( self, _event: input_event.InputEvent | None = None, notify_user: bool = True ) -> bool: """Switches between object mode and layout mode for line presentation.""" layout_mode = not settings_manager.get_manager().get_setting("layoutMode") if notify_user: if layout_mode: self.present_message(messages.MODE_LAYOUT) else: self.present_message(messages.MODE_OBJECT) settings_manager.get_manager().set_setting("layoutMode", layout_mode) return True def toggle_presentation_mode( self, event: input_event.InputEvent | None = None, document: Atspi.Accessible | None = None, notify_user: bool = True ) -> bool: """Switches between browse mode and focus mode.""" obj, _offset = self.utilities.get_caret_context(document) if self._in_focus_mode: parent = AXObject.get_parent(obj) if AXUtilities.is_list_box(parent): self.utilities.set_caret_context(parent, -1) elif AXUtilities.is_menu(parent): self.utilities.set_caret_context(AXObject.get_parent(parent), -1) if notify_user and not self._loading_content: self.present_message(messages.MODE_BROWSE) else: if not self.utilities.grab_focus_when_setting_caret(obj) \ and (self.get_caret_navigator().last_input_event_was_navigation_command() \ or self.get_structural_navigator().last_input_event_was_navigation_command() \ or self.get_table_navigator().last_input_event_was_navigation_command() \ or event): AXObject.grab_focus(obj) if notify_user: self.present_message(messages.MODE_FOCUS) self._in_focus_mode = not self._in_focus_mode self._focus_mode_is_sticky = False self._browse_mode_is_sticky = False reason = "toggling focus/browse mode" self.get_caret_navigator().suspend_commands(self, self._in_focus_mode, reason) self.get_structural_navigator().suspend_commands(self, self._in_focus_mode, reason) self.live_region_manager.suspend_commands(self, self._in_focus_mode, reason) self.get_table_navigator().suspend_commands(self, self._in_focus_mode, reason) return True def locus_of_focus_changed( self, event: Atspi.Event | None, old_focus: Atspi.Accessible | None, new_focus: Atspi.Accessible | None ) -> bool: """Handles changes of focus of interest. Returns True if this script did all needed work.""" tokens = ["WEB: Focus changing from", old_focus, "to", new_focus] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if new_focus and not AXObject.is_valid(new_focus): return True if new_focus and AXObject.is_dead(new_focus): return True document = self.utilities.get_top_level_document_for_object(new_focus) if not document and self.utilities.is_document(new_focus): document = new_focus sn_navigator = self.get_structural_navigator() last_command_was_struct_nav = sn_navigator.last_input_event_was_navigation_command() if not document: msg = "WEB: Locus of focus changed to non-document obj" debug.print_message(debug.LEVEL_INFO, msg, True) self._made_find_announcement = False self._in_focus_mode = False old_document = self.utilities.get_top_level_document_for_object(old_focus) if not document and self.utilities.is_document(old_focus): old_document = old_focus if old_focus and not old_document: msg = "WEB: Not refreshing grabs because we weren't in a document before" debug.print_message(debug.LEVEL_INFO, msg, True) return False if last_command_was_struct_nav and sn_navigator.get_mode(self) == NavigationMode.GUI: msg = "WEB: Not refreshing grabs: Last command was GUI structural navigation" debug.print_message(debug.LEVEL_INFO, msg, True) return False tokens = ["WEB: Refreshing grabs because we left document", old_document] debug.print_tokens(debug.LEVEL_INFO, tokens, True) reason = "locus of focus no longer in document" self.get_caret_navigator().suspend_commands(self, True, reason) assert self.live_region_manager is not None self.live_region_manager.suspend_commands(self, True, reason) self.get_structural_navigator().suspend_commands(self, True, reason) self.get_table_navigator().suspend_commands(self, True, reason) return False if self.get_flat_review_presenter().is_active(): self.get_flat_review_presenter().quit() caret_offset = 0 if self.utilities.in_find_container(old_focus) \ or (self.utilities.is_document(new_focus) \ and old_focus == focus_manager.get_manager().get_active_window()): context_obj, context_offset = self.utilities.get_caret_context(document) if context_obj and AXObject.is_valid(context_obj): new_focus, caret_offset = context_obj, context_offset if AXUtilities.is_unknown_or_redundant(new_focus): msg = "WEB: Event source has bogus role. Likely browser bug." debug.print_message(debug.LEVEL_INFO, msg, True) new_focus, _offset = self.utilities.first_context(new_focus, 0) if self.utilities.treat_as_text_object(new_focus): text_offset = AXText.get_caret_offset(new_focus) if 0 <= text_offset <= AXText.get_character_count(new_focus): caret_offset = text_offset self.utilities.set_caret_context(new_focus, caret_offset, document) self.update_braille(new_focus, documentFrame=document) contents = None args = {} last_command_was_caret_nav = \ self.get_caret_navigator().last_input_event_was_navigation_command() last_command_was_struct_nav = last_command_was_struct_nav \ or self.get_table_navigator().last_input_event_was_navigation_command() manager = input_event_manager.get_manager() last_command_was_line_nav = manager.last_event_was_line_navigation() \ and not last_command_was_caret_nav args["priorObj"] = old_focus if manager.last_event_was_mouse_button() and event \ and event.type.startswith("object:text-caret-moved"): msg = "WEB: Last input event was mouse button. Generating line." debug.print_message(debug.LEVEL_INFO, msg, True) contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset) elif self.utilities.is_content_editable_with_embedded_objects(new_focus) \ and (last_command_was_caret_nav \ or last_command_was_struct_nav or last_command_was_line_nav) \ and not (AXUtilities.is_table_cell(new_focus) and AXObject.get_name(new_focus)): tokens = ["WEB: New focus", new_focus, "content editable. Generating line."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset) elif self.utilities.is_anchor(new_focus): tokens = ["WEB: New focus", new_focus, "is anchor. Generating line."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) contents = self.utilities.get_line_contents_at_offset(new_focus, 0) elif input_event_manager.get_manager().last_event_was_page_navigation() \ and not AXTable.get_table(new_focus) \ and not AXUtilities.is_feed_article(new_focus): tokens = ["WEB: New focus", new_focus, "was scrolled to. Generating line."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset) elif self.utilities.is_focused_with_math_child(new_focus): tokens = ["WEB: New focus", new_focus, "has math child. Generating line."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset) elif AXUtilities.is_heading(new_focus): tokens = ["WEB: New focus", new_focus, "is heading. Generating object."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) contents = self.utilities.get_object_contents_at_offset(new_focus, 0) elif self.utilities.caret_moved_to_same_page_fragment(event, old_focus): assert event is not None tokens = ["WEB: Source", event.source, "is same page fragment. Generating line."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) contents = self.utilities.get_line_contents_at_offset(new_focus, 0) elif event and event.type.startswith("object:children-changed:remove") \ and self.utilities.is_focus_mode_widget(new_focus): tokens = ["WEB: New focus", new_focus, "is recovery from removed child. Generating speech."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) elif last_command_was_line_nav and not AXObject.is_valid(old_focus): msg = "WEB: Last input event was line nav; old_focus is invalid. Generating line." debug.print_message(debug.LEVEL_INFO, msg, True) contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset) elif last_command_was_line_nav and event \ and event.type.startswith("object:children-changed"): msg = "WEB: Last input event was line nav and children changed. Generating line." debug.print_message(debug.LEVEL_INFO, msg, True) contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset) else: tokens = ["WEB: New focus", new_focus, "is not a special case. Generating speech."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if new_focus and AXObject.is_dead(new_focus): msg = "WEB: New focus has since died" debug.print_message(debug.LEVEL_INFO, msg, True) if self._get_queued_event("object:state-changed:focused", True): msg = "WEB: Have matching focused event. Not speaking contents" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.should_interrupt_for_locus_of_focus_change(old_focus, new_focus, event): self.interrupt_presentation() if contents: self.speak_contents(contents, **args) else: utterances = self.speech_generator.generate_speech(new_focus, **args) speech.speak(utterances) if self.utilities.in_top_level_web_app(new_focus) and not self._browse_mode_is_sticky: announce = not self.utilities.in_document_content(old_focus) self.enable_sticky_focus_mode(None, announce) return True if not self._focus_mode_is_sticky \ and not self._browse_mode_is_sticky \ and self.use_focus_mode(new_focus, old_focus) != self._in_focus_mode: dummy_event = input_event.InputEvent("synthetic") self.toggle_presentation_mode(dummy_event, document) if not self.utilities.in_document_content(old_focus): if self._focus_mode_is_sticky: self.present_message(messages.MODE_FOCUS_IS_STICKY) return True sn_navigator.set_mode(self, NavigationMode.DOCUMENT) reason = "locus of focus now in document" self.get_caret_navigator().suspend_commands(self, self._in_focus_mode, reason) sn_navigator.suspend_commands(self, self._in_focus_mode, reason) self.live_region_manager.suspend_commands(self, self._in_focus_mode, reason) self.get_table_navigator().suspend_commands(self, self._in_focus_mode, reason) return True def on_active_changed(self, event: Atspi.Event) -> bool: """Callback for object:state-changed:active accessibility events.""" if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False if not event.detail1: msg = "WEB: Ignoring because event source is now inactive" debug.print_message(debug.LEVEL_INFO, msg, True) return True if AXUtilities.is_dialog_or_alert(event.source): msg = "WEB: Event handled: Setting locusOfFocus to event source" debug.print_message(debug.LEVEL_INFO, msg, True) focus_manager.get_manager().set_locus_of_focus(event, event.source) return True return False def on_busy_changed(self, event: Atspi.Event) -> bool: """Callback for object:state-changed:busy accessibility events.""" if AXComponent.has_no_size(event.source): msg = "WEB: Ignoring event from page with no size." debug.print_message(debug.LEVEL_INFO, msg, True) return True if not AXDocument.get_uri(event.source): msg = "WEB: Ignoring event from page with no URI." debug.print_message(debug.LEVEL_INFO, msg, True) return True AXUtilities.clear_all_cache_now(event.source, "busy-changed event.") if event.detail1 and self._loading_content: msg = "WEB: Ignoring: Already loading document content" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False active_document = self.utilities.active_document() if active_document and active_document != event.source: msg = "WEB: Ignoring: Event source is not active document" debug.print_message(debug.LEVEL_INFO, msg, True) return True focus = focus_manager.get_manager().get_locus_of_focus() if not AXUtilities.is_document_web(event.source) \ and not AXObject.is_ancestor(focus, event.source, True): msg = "WEB: Ignoring: Not document and not something we're in" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.get_document_for_object(AXObject.get_parent(event.source)): msg = "WEB: Ignoring: Event source is nested document" debug.print_message(debug.LEVEL_INFO, msg, True) return True obj, offset = self.utilities.get_caret_context() if not AXObject.is_valid(obj): self.utilities.clear_caret_context() should_present = True mgr = settings_manager.get_manager() if mgr.get_setting("onlySpeakDisplayedText"): should_present = False msg = "WEB: Not presenting due to settings" debug.print_message(debug.LEVEL_INFO, msg, True) elif not (AXUtilities.is_showing(event.source) or AXUtilities.is_visible(event.source)): should_present = False msg = "WEB: Not presenting because source is not showing or visible" debug.print_message(debug.LEVEL_INFO, msg, True) elif not AXDocument.get_uri(event.source): should_present = False msg = "WEB: Not presenting because source lacks URI" debug.print_message(debug.LEVEL_INFO, msg, True) elif not event.detail1 and self._in_focus_mode and AXObject.is_valid(obj): should_present = False tokens = ["WEB: Not presenting due to focus mode for", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) elif mgr.get_setting("speechVerbosityLevel") != settings.VERBOSITY_LEVEL_VERBOSE: should_present = not event.detail1 tokens = ["WEB: Brief verbosity set. Should present", obj, f": {should_present}"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if should_present and AXDocument.get_uri(event.source).startswith("http"): if event.detail1: self.present_message(messages.PAGE_LOADING_START) elif AXObject.get_name(event.source): if mgr.get_setting("speechVerbosityLevel") != settings.VERBOSITY_LEVEL_VERBOSE: msg = AXObject.get_name(event.source) else: msg = messages.PAGE_LOADING_END_NAMED % AXObject.get_name(event.source) self.present_message(msg, reset_styles=False) else: self.present_message(messages.PAGE_LOADING_END) self._loading_content = event.detail1 if event.detail1: return True self.utilities.clear_cached_objects() if AXObject.is_dead(obj): obj = None if not focus_manager.get_manager().focus_is_dead() \ and not self.utilities.in_document_content(focus) \ and AXUtilities.is_focused(focus): msg = "WEB: Not presenting content, focus is outside of document" debug.print_message(debug.LEVEL_INFO, msg, True) return True if settings_manager.get_manager().get_setting("pageSummaryOnLoad") and should_present: tokens = ["WEB: Getting page summary for", event.source] debug.print_tokens(debug.LEVEL_INFO, tokens, True) summary = AXDocument.get_document_summary(event.source) if summary: self.present_message(summary) obj, offset = self.utilities.get_caret_context() if not AXUtilities.is_busy(event.source) \ and self.utilities.is_top_level_web_app(event.source): tokens = ["WEB: Setting locusOfFocus to", obj, "with sticky focus mode"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus_manager.get_manager().set_locus_of_focus(event, obj) dummy_event = input_event.InputEvent("synthetic") self.enable_sticky_focus_mode(dummy_event, True) return True if self.use_focus_mode(obj) != self._in_focus_mode: dummy_event = input_event.InputEvent("synthetic") self.toggle_presentation_mode(dummy_event) if not obj: msg = "WEB: Could not get caret context" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.is_focus_mode_widget(obj): tokens = ["WEB: Setting locus of focus to focusModeWidget", obj] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus_manager.get_manager().set_locus_of_focus(event, obj) return True if self.utilities.is_link(obj) and AXUtilities.is_focused(obj): tokens = ["WEB: Setting locus of focus to focused link", obj, ". No SayAll."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus_manager.get_manager().set_locus_of_focus(event, obj) return True if offset > 0: tokens = ["WEB: Setting locus of focus to context obj", obj, ". No SayAll"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus_manager.get_manager().set_locus_of_focus(event, obj) return True if not AXUtilities.is_focused(focus_manager.get_manager().get_locus_of_focus()): tokens = ["WEB: Setting locus of focus to context obj", obj, "(no notification)"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus_manager.get_manager().set_locus_of_focus(event, obj, False) self.update_braille(obj) if AXDocument.get_document_uri_fragment(event.source): msg = "WEB: Not doing SayAll due to page fragment" debug.print_message(debug.LEVEL_INFO, msg, True) elif not settings_manager.get_manager().get_setting("sayAllOnLoad"): msg = "WEB: Not doing SayAll due to sayAllOnLoad being False" debug.print_message(debug.LEVEL_INFO, msg, True) self.speak_contents(self.utilities.get_line_contents_at_offset(obj, offset)) elif settings_manager.get_manager().get_setting("enableSpeech"): msg = "WEB: Doing SayAll" debug.print_message(debug.LEVEL_INFO, msg, True) self.get_say_all_presenter().say_all(self, None) else: msg = "WEB: Not doing SayAll due to enableSpeech being False" debug.print_message(debug.LEVEL_INFO, msg, True) return True def on_caret_moved(self, event: Atspi.Event) -> bool: """Callback for object:text-caret-moved accessibility events.""" reason = AXUtilities.get_text_event_reason(event) document = self.utilities.get_top_level_document_for_object(event.source) if not document: if self.utilities.event_is_browser_ui_noise_deprecated(event): msg = "WEB: Ignoring event believed to be browser UI noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.event_is_browser_ui_autocomplete_noise_deprecated(event): msg = "WEB: Ignoring event believed to be browser UI autocomplete noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False obj, offset = self.utilities.get_caret_context(document, False, False) tokens = ["WEB: Context: ", obj, ", ", offset] debug.print_tokens(debug.LEVEL_INFO, tokens, True) # TODO - JD: Make this a TextEventReason. if self.get_caret_navigator().last_input_event_was_navigation_command(): msg = "WEB: Event ignored: Last command was caret nav" debug.print_message(debug.LEVEL_INFO, msg, True) return True # TODO - JD: Make this a TextEventReason. if self.get_structural_navigator().last_input_event_was_navigation_command(): msg = "WEB: Event ignored: Last command was struct nav" debug.print_message(debug.LEVEL_INFO, msg, True) return True # TODO - JD: Make this a TextEventReason. if self.get_table_navigator().last_input_event_was_navigation_command(): msg = "WEB: Event ignored: Last command was table nav" debug.print_message(debug.LEVEL_INFO, msg, True) return True if reason == TextEventReason.SAY_ALL: msg = "WEB: Event handled: SayAll triggered" debug.print_message(debug.LEVEL_INFO, msg, True) return True if reason == TextEventReason.MOUSE_PRIMARY_BUTTON: if (event.source, event.detail1) == (obj, offset): msg = "WEB: Event is for current caret context." debug.print_message(debug.LEVEL_INFO, msg, True) return True if (event.source, event.detail1) == self._last_mouse_button_context: msg = "WEB: Event is for last mouse button context." debug.print_message(debug.LEVEL_INFO, msg, True) return True self._last_mouse_button_context = event.source, event.detail1 msg = "WEB: Event handled: Last command was mouse button" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.set_caret_context(event.source, event.detail1) notify = AXObject.find_ancestor_inclusive(obj, AXUtilities.is_entry) is None focus_manager.get_manager().set_locus_of_focus(event, event.source, notify, True) return True if reason == TextEventReason.FOCUS_CHANGE: msg = "WEB: Event ignored: Caret moved due to focus change." debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.in_find_container(): msg = "WEB: Event handled: Presenting find results" debug.print_message(debug.LEVEL_INFO, msg, True) self._present_find_results(event.source, event.detail1) return True if not self.utilities.event_is_from_locus_of_focus_document(event): msg = "WEB: Event ignored: Not from locus of focus document" debug.print_message(debug.LEVEL_INFO, msg, True) return True if reason == TextEventReason.UI_UPDATE: msg = "WEB: Event ignored: Caret moved due to UI update." debug.print_message(debug.LEVEL_INFO, msg, True) return True if reason in [TextEventReason.TYPING, TextEventReason.TYPING_ECHOABLE]: msg = "WEB: Event handled: Updating position due to insertion" debug.print_message(debug.LEVEL_INFO, msg, True) focus_manager.get_manager().set_last_cursor_position(event.source, event.detail1) return True if reason in (TextEventReason.DELETE, TextEventReason.BACKSPACE): msg = "WEB: Event handled: Updating position due to deletion" debug.print_message(debug.LEVEL_INFO, msg, True) focus_manager.get_manager().set_last_cursor_position(event.source, event.detail1) return True focus = focus_manager.get_manager().get_locus_of_focus() i_e_manager = input_event_manager.get_manager() if self.utilities.is_item_for_editable_combo_box(focus, event.source) \ and not i_e_manager.last_event_was_character_navigation() \ and not i_e_manager.last_event_was_line_boundary_navigation(): msg = "WEB: Event ignored: Editable combobox noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.event_is_autocomplete_noise_deprecated(event, document): msg = "WEB: Event ignored: Autocomplete noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self._in_focus_mode and self.utilities.caret_moved_outside_active_grid(event): msg = "WEB: Event ignored: Caret moved outside active grid during focus mode" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.treat_event_as_spinner_value_change_deprecated(event): msg = "WEB: Event handled as the value-change event we wish we'd get" debug.print_message(debug.LEVEL_INFO, msg, True) self.update_braille(event.source) self._present_caret_moved_event(event) return True if not self.utilities.treat_as_text_object(event.source) \ and not AXUtilities.is_editable(event.source): msg = "WEB: Event ignored: Was for non-editable object we're treating as textless" debug.print_message(debug.LEVEL_INFO, msg, True) return True notify = force = handled = False AXObject.clear_cache(event.source, False, "Updating state for caret moved event.") if self._in_focus_mode: obj, offset = event.source, event.detail1 else: obj, offset = self.utilities.first_context(event.source, event.detail1) if reason == TextEventReason.NAVIGATION_BY_PAGE: msg = "WEB: Caret moved due to scrolling." debug.print_message(debug.LEVEL_INFO, msg, True) notify = force = handled = True elif self.utilities.caret_moved_to_same_page_fragment(event): msg = "WEB: Caret moved to fragment." debug.print_message(debug.LEVEL_INFO, msg, True) notify = force = handled = True elif event.source != focus and AXUtilities.is_editable(event.source) and \ (AXUtilities.is_focused(event.source) or not AXUtilities.is_focusable(event.source)): msg = "WEB: Editable object is not (yet) the locus of focus." debug.print_message(debug.LEVEL_INFO, msg, True) notify = force = handled = \ i_e_manager.last_event_was_line_navigation() or \ i_e_manager.last_event_was_return() elif i_e_manager.last_event_was_caret_navigation(): msg = "WEB: Caret moved due to native caret navigation." debug.print_message(debug.LEVEL_INFO, msg, True) tokens = ["WEB: Setting context and focus to: ", obj, ", ", offset, f", notify: {notify}"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self.utilities.set_caret_context(obj, offset, document) focus_manager.get_manager().set_locus_of_focus(event, obj, notify, force) return handled def on_checked_changed(self, event: Atspi.Event) -> bool: """Callback for object:state-changed:checked accessibility events.""" msg = "WEB: This event is is handled by the toolkit or default script." debug.print_message(debug.LEVEL_INFO, msg, True) return False def on_children_added(self, event: Atspi.Event) -> bool: """Callback for object:children-changed:add accessibility events.""" AXUtilities.clear_all_cache_now(event.source, "children-changed event.") if self.utilities.event_is_browser_ui_noise_deprecated(event): msg = "WEB: Ignoring event believed to be browser UI noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True is_live_region = AXUtilities.is_live_region(event.source) document = self.utilities.get_top_level_document_for_object(event.source) if document and not is_live_region: focus = focus_manager.get_manager().get_locus_of_focus() if event.source == focus: msg = "WEB: Dumping cache: source is focus" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.dump_cache(document, preserve_context=True) elif focus_manager.get_manager().focus_is_dead(): msg = "WEB: Dumping cache: dead focus" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.dump_cache(document, preserve_context=True) elif AXObject.find_ancestor(focus, lambda x: x == event.source): msg = "WEB: Dumping cache: source is ancestor of focus" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.dump_cache(document, preserve_context=True) else: msg = "WEB: Not dumping full cache" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.clear_cached_objects() elif is_live_region: msg = "WEB: Ignoring event from live region." debug.print_message(debug.LEVEL_INFO, msg, True) return True else: msg = "WEB: Could not get document for event source" debug.print_message(debug.LEVEL_INFO, msg, True) return False if self._loading_content: msg = "WEB: Ignoring because document content is being loaded." debug.print_message(debug.LEVEL_INFO, msg, True) return True if not AXObject.is_valid(document): tokens = ["WEB: Ignoring because", document, "is not valid."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True if AXUtilities.is_busy(document): tokens = ["WEB: Ignoring because", document, "is busy."] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True if self.utilities.handle_event_from_context_replicant(event, event.any_data): msg = "WEB: Event handled by updating locusOfFocus and context to child." debug.print_message(debug.LEVEL_INFO, msg, True) return True if AXUtilities.is_alert(event.any_data): msg = "WEB: Presenting event.any_data" debug.print_message(debug.LEVEL_INFO, msg, True) self.present_object(event.any_data, interrupt=True) focused = AXUtilities.get_focused_object(event.any_data) if focused: notify = not self.utilities.treat_as_text_object(focused) tokens = ["WEB: Setting locusOfFocus and caret context to", focused] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus_manager.get_manager().set_locus_of_focus(event, focused, notify) self.utilities.set_caret_context(focused, 0) return True return False def on_children_removed(self, event: Atspi.Event) -> bool: """Callback for object:children-changed:removed accessibility events.""" AXUtilities.clear_all_cache_now(event.source, "children-changed event.") if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content." debug.print_message(debug.LEVEL_INFO, msg, True) return False if self._loading_content: msg = "WEB: Ignoring because document content is being loaded." debug.print_message(debug.LEVEL_INFO, msg, True) return True if AXUtilities.is_live_region(event.source): if self.utilities.handle_event_for_removed_child(event): msg = "WEB: Event handled for removed live-region child." debug.print_message(debug.LEVEL_INFO, msg, True) else: msg = "WEB: Ignoring removal from live region." debug.print_message(debug.LEVEL_INFO, msg, True) return True document = self.utilities.get_top_level_document_for_object(event.source) if document: focus = focus_manager.get_manager().get_locus_of_focus() if event.source == focus: msg = "WEB: Dumping cache: source is focus" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.dump_cache(document, preserve_context=True) elif focus_manager.get_manager().focus_is_dead(): msg = "WEB: Dumping cache: dead focus" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.dump_cache(document, preserve_context=True) elif AXObject.find_ancestor(focus, lambda x: x == event.source): msg = "WEB: Dumping cache: source is ancestor of focus" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.dump_cache(document, preserve_context=True) else: msg = "WEB: Not dumping full cache" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.clear_cached_objects() if self.utilities.handle_event_for_removed_child(event): msg = "WEB: Event handled for removed child." debug.print_message(debug.LEVEL_INFO, msg, True) return True return False def on_column_reordered(self, event: Atspi.Event) -> bool: """Callback for object:column-reordered accessibility events.""" if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False focus = focus_manager.get_manager().get_locus_of_focus() if event.source != AXTable.get_table(focus): msg = "WEB: focus is not in this table" debug.print_message(debug.LEVEL_INFO, msg, True) return False self.present_message(messages.TABLE_REORDERED_COLUMNS) header = AXObject.find_ancestor_inclusive(focus, AXUtilities.is_table_header) msg = AXTable.get_presentable_sort_order_from_header(header, True) if msg: self.present_message(msg) return True def on_document_load_complete(self, event: Atspi.Event) -> bool: """Callback for document:load-complete accessibility events.""" if AXComponent.has_no_size(event.source): msg = "WEB: Ignoring event from page with no size." debug.print_message(debug.LEVEL_INFO, msg, True) return True uri = AXDocument.get_uri(event.source) if not uri: msg = "WEB: Ignoring event from page with no URI." debug.print_message(debug.LEVEL_INFO, msg, True) return True if uri.startswith("moz-extension"): msg = f"WEB: Ignoring event from page with URI: {uri}" debug.print_message(debug.LEVEL_INFO, msg, True) return True AXUtilities.clear_all_cache_now(event.source, "load-complete event.") if self.utilities.get_document_for_object(AXObject.get_parent(event.source)): msg = "WEB: Ignoring: Event source is nested document" debug.print_message(debug.LEVEL_INFO, msg, True) return True msg = "WEB: Updating loading state and resetting live regions" debug.print_message(debug.LEVEL_INFO, msg, True) self._loading_content = False self.live_region_manager.reset() return True def on_document_load_stopped(self, event: Atspi.Event) -> bool: """Callback for document:load-stopped accessibility events.""" if self.utilities.get_document_for_object(AXObject.get_parent(event.source)): msg = "WEB: Ignoring: Event source is nested document" debug.print_message(debug.LEVEL_INFO, msg, True) return True msg = "WEB: Updating loading state" debug.print_message(debug.LEVEL_INFO, msg, True) self._loading_content = False return True def on_document_reload(self, event: Atspi.Event) -> bool: """Callback for document:reload accessibility events.""" if self.utilities.get_document_for_object(AXObject.get_parent(event.source)): msg = "WEB: Ignoring: Event source is nested document" debug.print_message(debug.LEVEL_INFO, msg, True) return True msg = "WEB: Updating loading state" debug.print_message(debug.LEVEL_INFO, msg, True) self._loading_content = True return True def on_expanded_changed(self, event: Atspi.Event) -> bool: """Callback for object:state-changed:expanded accessibility events.""" if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False focus = focus_manager.get_manager().get_locus_of_focus() obj, offset = self.utilities.get_caret_context(search_if_needed=False) tokens = ["WEB: Caret context is", obj, ", ", offset] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not AXObject.is_valid(obj) and event.source == focus: msg = "WEB: Setting caret context to event source" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.set_caret_context(event.source, 0) return False def on_focused_changed(self, event: Atspi.Event) -> bool: """Callback for object:state-changed:focused accessibility events.""" if not event.detail1: msg = "WEB: Ignoring because event source lost focus" debug.print_message(debug.LEVEL_INFO, msg, True) return True document = self.utilities.get_document_for_object(event.source) if not document: msg = "WEB: Could not get document for event source" debug.print_message(debug.LEVEL_INFO, msg, True) return False if focus_manager.get_manager().in_say_all(): msg = "WEB: Ignoring focus change during say all" debug.print_message(debug.LEVEL_INFO, msg, True) return True focus = focus_manager.get_manager().get_locus_of_focus() prev_document = self.utilities.get_document_for_object(focus) if prev_document != document: tokens = ["WEB: document changed from", prev_document, "to", document] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if AXUtilities.is_link(event.source) and AXObject.is_ancestor(focus, event.source): msg = "WEB: Ignoring focus change on link ancestor of focus" debug.print_message(debug.LEVEL_INFO, msg, True) return True if AXObject.find_ancestor(event.source, AXUtilities.is_embedded): if self._browse_mode_is_sticky: msg = "WEB: Embedded descendant claimed focus, but browse mode is sticky" debug.print_message(debug.LEVEL_INFO, msg, True) elif AXUtilities.is_tool_tip(event.source) \ and AXObject.find_ancestor(focus, lambda x: x == event.source): msg = "WEB: Event believed to be side effect of tooltip navigation." debug.print_message(debug.LEVEL_INFO, msg, True) return True else: msg = "WEB: Event handled: Setting locusOfFocus to embedded descendant" debug.print_message(debug.LEVEL_INFO, msg, True) if self.utilities.should_interrupt_for_locus_of_focus_change( focus, event.source, event): self.interrupt_presentation() focus_manager.get_manager().set_locus_of_focus(event, event.source) return True if AXUtilities.is_editable(event.source): msg = "WEB: Event source is editable" debug.print_message(debug.LEVEL_INFO, msg, True) return False if AXUtilities.is_dialog_or_alert(event.source): if AXObject.is_ancestor(focus, event.source, True): msg = "WEB: Ignoring event from ancestor of focus" debug.print_message(debug.LEVEL_INFO, msg, True) else: msg = "WEB: Event handled: Setting locusOfFocus to event source" debug.print_message(debug.LEVEL_INFO, msg, True) focus_manager.get_manager().set_locus_of_focus(event, event.source) return True if self.utilities.handle_event_from_context_replicant(event, event.source): msg = "WEB: Event handled by updating locusOfFocus and context to source." debug.print_message(debug.LEVEL_INFO, msg, True) return True obj, offset = self.utilities.get_caret_context() tokens = ["WEB: Caret context is", obj, ", ", offset] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not AXObject.is_valid(obj) or prev_document != document: tokens = ["WEB: Clearing context - obj", obj, "is not valid or document changed"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self.utilities.clear_caret_context() obj, offset = self.utilities.search_for_caret_context(event.source) if obj: notify = self.utilities.in_find_container(focus) tokens = ["WEB: Updating focus and context to", obj, ", ", offset] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus_manager.get_manager().set_locus_of_focus(event, obj, notify) if not notify and prev_document is None: reason = "updating locus of focus without notification" self.get_caret_navigator().suspend_commands(self, self._in_focus_mode, reason) self.get_structural_navigator().suspend_commands( self, self._in_focus_mode, reason) self.live_region_manager.suspend_commands(self, self._in_focus_mode, reason) self.get_table_navigator().suspend_commands(self, self._in_focus_mode, reason) self.utilities.set_caret_context(obj, offset) else: msg = "WEB: Search for caret context failed" debug.print_message(debug.LEVEL_INFO, msg, True) if self.get_caret_navigator().last_input_event_was_navigation_command(): msg = "WEB: Event ignored: Last command was caret nav" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.get_structural_navigator().last_input_event_was_navigation_command(): msg = "WEB: Event ignored: Last command was struct nav" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.get_table_navigator().last_input_event_was_navigation_command(): msg = "WEB: Event ignored: Last command was table nav" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not (AXUtilities.is_focusable(event.source) and AXUtilities.is_focused(event.source)): msg = "WEB: Event ignored: Source is not focusable or focused" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not AXUtilities.is_document(event.source): msg = "WEB: Deferring to other scripts for handling non-document source" debug.print_message(debug.LEVEL_INFO, msg, True) return False if not obj: msg = "WEB: Unable to get valid context object" debug.print_message(debug.LEVEL_INFO, msg, True) return False if input_event_manager.get_manager().last_event_was_page_navigation(): msg = "WEB: Event handled: Focus changed due to scrolling" debug.print_message(debug.LEVEL_INFO, msg, True) focus_manager.get_manager().set_locus_of_focus(event, obj) self.utilities.set_caret_context(obj, offset) return True # TODO - JD: Can this logic be removed? was_focused = AXUtilities.is_focused(obj) AXObject.clear_cache(obj, False, "Sanity-checking focused state.") is_focused = AXUtilities.is_focused(obj) if was_focused != is_focused: tokens = ["WEB: Focused state of", obj, "changed to", is_focused] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False if self.utilities.is_anchor(obj): cause = "Context is anchor" elif not (self.utilities.is_link(obj) and not is_focused): cause = "Context is not a non-focused link" elif self.utilities.is_child_of_current_fragment(obj): cause = "Context is child of current fragment" elif document == event.source and AXDocument.get_document_uri_fragment(event.source): cause = "Document URI is fragment" else: return False tokens = ["WEB: Event handled: Setting locusOfFocus to", obj, "(", cause, ")"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) focus_manager.get_manager().set_locus_of_focus(event, obj) return True def on_mouse_button(self, event: Atspi.Event) -> bool: """Callback for mouse:button accessibility events.""" return False def on_name_changed(self, event: Atspi.Event) -> bool: """Callback for object:property-change:accessible-name events.""" if self.utilities.event_is_browser_ui_noise_deprecated(event): msg = "WEB: Ignoring event believed to be browser UI noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True return False def on_row_reordered(self, event: Atspi.Event) -> bool: """Callback for object:row-reordered accessibility events.""" if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False focus = focus_manager.get_manager().get_locus_of_focus() if event.source != AXTable.get_table(focus): msg = "WEB: focus is not in this table" debug.print_message(debug.LEVEL_INFO, msg, True) return False self.present_message(messages.TABLE_REORDERED_ROWS) header = AXObject.find_ancestor_inclusive(focus, AXUtilities.is_table_header) msg = AXTable.get_presentable_sort_order_from_header(header, True) if msg: self.present_message(msg) return True def on_selected_changed(self, event: Atspi.Event) -> bool: """Callback for object:state-changed:selected accessibility events.""" if self.utilities.event_is_browser_ui_autocomplete_noise_deprecated(event): msg = "WEB: Ignoring event believed to be browser UI autocomplete noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True focus = focus_manager.get_manager().get_locus_of_focus() if self.utilities.event_is_browser_ui_page_switch(event): msg = "WEB: Event believed to be browser UI page switch" debug.print_message(debug.LEVEL_INFO, msg, True) if event.detail1: # https://bugzilla.mozilla.org/show_bug.cgi?id=1867044 AXObject.clear_cache(event.source, False, "Work around Gecko bug.") AXUtilities.clear_all_cache_now(reason=msg) self.present_object(event.source, priorObj=focus, interrupt=True) return True if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False if focus != event.source: msg = "WEB: Ignoring because event source is not locusOfFocus" debug.print_message(debug.LEVEL_INFO, msg, True) return True return False def on_selection_changed(self, event: Atspi.Event) -> bool: """Callback for object:selection-changed accessibility events.""" if self.utilities.event_is_browser_ui_autocomplete_noise_deprecated(event): msg = "WEB: Ignoring event believed to be browser UI autocomplete noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.event_is_browser_ui_page_switch(event): msg = "WEB: Ignoring event believed to be browser UI page switch" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False if not self.utilities.in_document_content(focus_manager.get_manager().get_locus_of_focus()): msg = "WEB: Event ignored: locusOfFocus is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not self.utilities.event_is_from_locus_of_focus_document(event): msg = "WEB: Event ignored: Not from locus of focus document" debug.print_message(debug.LEVEL_INFO, msg, True) return True if AXObject.find_ancestor(event.source, AXUtilities.is_embedded): if self._in_focus_mode: # Because we cannot count on the app firing the right state-changed events # for descendants. AXObject.clear_cache(event.source, True, "Workaround for missing events on descendants.") msg = "WEB: Event source is embedded descendant and we're in focus mode" debug.print_message(debug.LEVEL_INFO, msg, True) return False msg = "WEB: Event source is embedded descendant and we're in browse mode" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.event_is_irrelevant_selection_changed_event(event): msg = "WEB: Event ignored: Irrelevant" debug.print_message(debug.LEVEL_INFO, msg, True) return True obj, _offset = self.utilities.get_caret_context() ancestor = AXObject.get_common_ancestor(obj, event.source) if ancestor and self.utilities.is_text_block_element(ancestor): msg = "WEB: Ignoring: Common ancestor of context and event source is text block" debug.print_message(debug.LEVEL_INFO, msg, True) return True return False def on_showing_changed(self, event: Atspi.Event) -> bool: """Callback for object:state-changed:showing accessibility events.""" if event.detail1 and self.utilities.is_browser_ui_alert(event.source): msg = "WEB: Event handled: Presenting event source" debug.print_message(debug.LEVEL_INFO, msg, True) self.present_object(event.source, interrupt=True) return True if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False return True def on_text_attributes_changed(self, event: Atspi.Event) -> bool: """Callback for object:text-attributes-changed accessibility events.""" msg = "WEB: This event is is handled by the toolkit or default script." debug.print_message(debug.LEVEL_INFO, msg, True) return False def on_text_deleted(self, event: Atspi.Event) -> bool: """Callback for object:text-changed:delete accessibility events.""" reason = AXUtilities.get_text_event_reason(event) if reason == TextEventReason.PAGE_SWITCH: msg = "WEB: Deletion is believed to be due to page switch" debug.print_message(debug.LEVEL_INFO, msg, True) return True if AXUtilities.is_live_region(event.source): msg = "WEB: Ignoring deletion from live region" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) if reason == TextEventReason.UI_UPDATE: msg = "WEB: Ignoring event believed to be browser UI update" debug.print_message(debug.LEVEL_INFO, msg, True) return True return False if reason == TextEventReason.SPIN_BUTTON_VALUE_CHANGE: msg = "WEB: Ignoring: Event believed to be spin button value change" debug.print_message(debug.LEVEL_INFO, msg, True) return True if reason == TextEventReason.AUTO_DELETION: msg = "WEB: Ignoring event believed to be auto deletion" debug.print_message(debug.LEVEL_INFO, msg, True) return True msg = "WEB: Clearing content cache due to text deletion" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.clear_content_cache() if reason in (TextEventReason.DELETE, TextEventReason.BACKSPACE): msg = "WEB: Event believed to be due to editable text deletion" debug.print_message(debug.LEVEL_INFO, msg, True) return False if reason in [TextEventReason.TYPING, TextEventReason.TYPING_ECHOABLE]: msg = "WEB: Ignoring event believed to be due to text insertion" debug.print_message(debug.LEVEL_INFO, msg, True) return True obj, _offset = self.utilities.get_caret_context(get_replicant=False) if obj and obj != event.source \ and not AXObject.find_ancestor(obj, lambda x: x == event.source): tokens = ["WEB: Ignoring event because it isn't", obj, "or its ancestor"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return True if not AXObject.is_valid(obj): if self.utilities.is_link(obj): msg = "WEB: Focused link deleted. Taking no further action." debug.print_message(debug.LEVEL_INFO, msg, True) return True obj, _offset = self.utilities.get_caret_context(get_replicant=True) if obj: focus_manager.get_manager().set_locus_of_focus(event, obj, notify_script=False) if not AXObject.is_valid(obj): msg = "WEB: Unable to get valid context object" debug.print_message(debug.LEVEL_INFO, msg, True) if not AXUtilities.is_editable(event.source) \ and not self.utilities.is_content_editable_with_embedded_objects(event.source): msg = "WEB: Done processing non-editable source" debug.print_message(debug.LEVEL_INFO, msg, True) return True return False def on_text_inserted(self, event: Atspi.Event) -> bool: """Callback for object:text-changed:insert accessibility events.""" reason = AXUtilities.get_text_event_reason(event) if reason == TextEventReason.PAGE_SWITCH: msg = "WEB: Insertion is believed to be due to page switch" debug.print_message(debug.LEVEL_INFO, msg, True) return True if AXUtilities.is_live_region(event.source): if self.utilities.handle_as_live_region(event): msg = "WEB: Event to be handled as live region" debug.print_message(debug.LEVEL_INFO, msg, True) self.live_region_manager.handleEvent(event) return True msg = "WEB: Ignoring because live region event not to be handled." debug.print_message(debug.LEVEL_INFO, msg, True) return True if reason == TextEventReason.CHILDREN_CHANGE: msg = "WEB: Ignoring: Event believed to be due to children change" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) if reason == TextEventReason.UI_UPDATE: msg = "WEB: Ignoring event believed to be browser UI update" debug.print_message(debug.LEVEL_INFO, msg, True) return True return False if reason == TextEventReason.SPIN_BUTTON_VALUE_CHANGE: msg = "WEB: Ignoring: Event believed to be spin button value change" debug.print_message(debug.LEVEL_INFO, msg, True) return True if reason == TextEventReason.AUTO_INSERTION_PRESENTABLE: msg = "WEB: Ignoring: Event believed to be auto insertion" debug.print_message(debug.LEVEL_INFO, msg, True) return False msg = "WEB: Clearing content cache due to text insertion" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.clear_content_cache() document = self.utilities.get_top_level_document_for_object(event.source) if focus_manager.get_manager().focus_is_dead(): msg = "WEB: Dumping cache: dead focus" debug.print_message(debug.LEVEL_INFO, msg, True) self.utilities.dump_cache(document, preserve_context=True) if AXUtilities.is_focused(event.source): msg = "WEB: Event handled: Setting locusOfFocus to event source" debug.print_message(debug.LEVEL_INFO, msg, True) focus_manager.get_manager().set_locus_of_focus(None, event.source, force=True) return True if not self.utilities.treat_as_text_object(event.source): msg = "WEB: Ignoring: Event source is not a text object" debug.print_message(debug.LEVEL_INFO, msg, True) return True source_is_focus = event.source == focus_manager.get_manager().get_locus_of_focus() if not AXUtilities.is_editable(event.source): if not source_is_focus: msg = "WEB: Done processing non-editable, non-locusOfFocus source" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.is_clickable_element(event.source): msg = "WEB: Event handled: Re-setting locusOfFocus to changed clickable" debug.print_message(debug.LEVEL_INFO, msg, True) focus_manager.get_manager().set_locus_of_focus(None, event.source, force=True) return True if not source_is_focus and AXUtilities.is_text_input(event.source) \ and AXUtilities.is_focused(event.source): msg = "WEB: Focused entry is not the locus of focus. Waiting for focus event." debug.print_message(debug.LEVEL_INFO, msg, True) return True return False def on_text_selection_changed(self, event: Atspi.Event) -> bool: """Callback for object:text-selection-changed accessibility events.""" _reason = AXUtilities.get_text_event_reason(event) if self.utilities.event_is_browser_ui_noise_deprecated(event): msg = "WEB: Ignoring event believed to be browser UI noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not self.utilities.in_document_content(event.source): msg = "WEB: Event source is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return False focus = focus_manager.get_manager().get_locus_of_focus() if not self.utilities.in_document_content(focus): msg = "WEB: Locus of focus is not in document content" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.event_is_autocomplete_noise_deprecated(event): msg = "WEB: Ignoring: Event believed to be autocomplete noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.event_is_spinner_noise_deprecated(event): msg = "WEB: Ignoring: Event believed to be spinner noise" debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.event_is_for_non_navigable_text_object(event): msg = "WEB: Ignoring event for non-navigable text object" debug.print_message(debug.LEVEL_INFO, msg, True) return True if not self.utilities.treat_as_text_object(event.source): msg = "WEB: Ignoring: Event source is not a text object" debug.print_message(debug.LEVEL_INFO, msg, True) return True if event.source != focus and AXUtilities.is_text_input(event.source) \ and AXUtilities.is_focused(event.source): msg = "WEB: Focused entry is not the locus of focus. Waiting for focus event." debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.utilities.is_content_editable_with_embedded_objects(event.source): msg = "WEB: In content editable with embedded objects" debug.print_message(debug.LEVEL_INFO, msg, True) return False if self.get_structural_navigator().last_input_event_was_navigation_command(): msg = "WEB: Ignoring: Last input event was structural navigation command." debug.print_message(debug.LEVEL_INFO, msg, True) return True if self.get_table_navigator().last_input_event_was_navigation_command(): msg = "WEB: Ignoring: Last input event was table navigation command." debug.print_message(debug.LEVEL_INFO, msg, True) return True char = AXText.get_character_at_offset(event.source)[0] manager = input_event_manager.get_manager() if char == "\ufffc" and not manager.last_event_was_caret_selection() \ and not manager.last_event_was_command(): msg = "WEB: Ignoring: Not selecting and event offset is at embedded object" debug.print_message(debug.LEVEL_INFO, msg, True) return True return False def on_window_activated(self, event: Atspi.Event) -> bool: """Callback for window:activate accessibility events.""" msg = "WEB: Deferring to app/toolkit script" debug.print_message(debug.LEVEL_INFO, msg, True) return False def on_window_deactivated(self, event: Atspi.Event) -> bool: """Callback for window:deactivate accessibility events.""" msg = "WEB: Clearing command state" debug.print_message(debug.LEVEL_INFO, msg, True) self._last_mouse_button_context = None, -1 return False