# Orca # # Copyright 2005-2009 Sun Microsystems Inc. # Copyright 2011-2025 Igalia, S.L. # Author: Joanmarie Diggs # # 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-lines # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments # pylint: disable=too-many-public-methods """Implements structural navigation.""" from __future__ import annotations from enum import Enum from typing import TYPE_CHECKING, Any import gi gi.require_version("Atspi", "2.0") from gi.repository import Atspi from . import ( cmdnames, command_manager, dbus_service, debug, focus_manager, gsettings_registry, guilabels, input_event_manager, keybindings, live_region_presenter, messages, object_properties, orca_gui_navlist, presentation_manager, say_all_presenter, script_manager, ) from .ax_hypertext import AXHypertext from .ax_object import AXObject from .ax_table import AXTable from .ax_text import AXText from .ax_utilities import AXUtilities if TYPE_CHECKING: from collections.abc import Callable from .input_event import InputEvent from .scripts import default class NavigationMode(Enum): """Represents the structural navigation modes available.""" OFF = "OFF" DOCUMENT = "DOCUMENT" GUI = "GUI" @gsettings_registry.get_registry().gsettings_schema( "org.gnome.Orca.StructuralNavigation", name="structural-navigation", ) class StructuralNavigator: """Implements the structural navigation support available to scripts.""" _SCHEMA = "structural-navigation" KEY_WRAPS = "wraps" KEY_LARGE_OBJECT_TEXT_LENGTH = "large-object-text-length" KEY_ENABLED = "enabled" KEY_TRIGGERS_FOCUS_MODE = "triggers-focus-mode" def _get_setting(self, key: str, gtype: str, default: Any) -> Any: """Returns the dconf value for key, or default if not in dconf.""" return gsettings_registry.get_registry().layered_lookup( self._SCHEMA, key, gtype, default=default, ) def __init__(self) -> None: self._last_input_event: InputEvent | None = None # To make it possible for focus mode to suspend this navigation without # changing the user's preferred setting. self._suspended: bool = False self._mode_for_script: dict[default.Script, NavigationMode] = {} self._previous_mode_for_script: dict[default.Script, NavigationMode] = {} self._initialized: bool = False msg = "STRUCTURAL NAVIGATOR: Registering D-Bus commands." debug.print_message(debug.LEVEL_INFO, msg, True) controller = dbus_service.get_remote_controller() controller.register_decorated_module("StructuralNavigator", self) # pylint: disable-next=too-many-locals def set_up_commands(self) -> None: """Sets up commands with CommandManager.""" if self._initialized: return self._initialized = True manager = command_manager.get_manager() group_label = guilabels.KB_GROUP_STRUCTURAL_NAVIGATION # Mode cycle command kb_z = keybindings.KeyBinding("z", keybindings.ORCA_MODIFIER_MASK) manager.add_command( command_manager.KeyboardCommand( "structural_navigator_mode_cycle", self.cycle_mode, group_label, cmdnames.STRUCTURAL_NAVIGATION_MODE_CYCLE, desktop_keybinding=kb_z, laptop_keybinding=kb_z, is_group_toggle=True, ), ) # Navigation bindings - (key, prev_mod, next_mod, list_mod, base_name) nav_bindings = [ ( "q", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "blockquote", ), ( "b", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "button", ), ( "x", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "checkbox", ), ( "c", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "combobox", ), ( "e", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "entry", ), ( "f", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "form_field", ), ( "h", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "heading", ), ( "g", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "image", ), ( "m", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "landmark", ), ( "l", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "list", ), ( "i", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "list_item", ), ( "p", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "paragraph", ), ( "r", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "radio_button", ), ( "t", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "table", ), ( "k", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "link", ), ( "u", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "unvisited_link", ), ( "v", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "visited_link", ), ( "o", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "large_object", ), ( "a", keybindings.SHIFT_MODIFIER_MASK, keybindings.NO_MODIFIER_MASK, keybindings.SHIFT_ALT_MODIFIER_MASK, "clickable", ), ] # Build command name -> keybinding mapping cmd_bindings: dict[str, keybindings.KeyBinding | None] = {} for key, prev_mod, next_mod, list_mod, base_name in nav_bindings: cmd_bindings[f"previous_{base_name}"] = keybindings.KeyBinding(key, prev_mod) cmd_bindings[f"next_{base_name}"] = keybindings.KeyBinding(key, next_mod) # Handle plurals for list commands if base_name == "entry": plural = "entries" elif base_name in ("checkbox", "combobox"): plural = f"{base_name}es" else: plural = f"{base_name}s" cmd_bindings[f"list_{plural}"] = keybindings.KeyBinding(key, list_mod) # Additional bindings cmd_bindings["previous_separator"] = keybindings.KeyBinding( "s", keybindings.SHIFT_MODIFIER_MASK, ) cmd_bindings["next_separator"] = keybindings.KeyBinding("s", keybindings.NO_MODIFIER_MASK) cmd_bindings["previous_live_region"] = keybindings.KeyBinding( "d", keybindings.SHIFT_MODIFIER_MASK, ) cmd_bindings["next_live_region"] = keybindings.KeyBinding("d", keybindings.NO_MODIFIER_MASK) cmd_bindings["last_live_region"] = keybindings.KeyBinding("y", keybindings.NO_MODIFIER_MASK) cmd_bindings["container_start"] = keybindings.KeyBinding( "comma", keybindings.SHIFT_MODIFIER_MASK, ) cmd_bindings["container_end"] = keybindings.KeyBinding( "comma", keybindings.NO_MODIFIER_MASK, ) # Commands with no bindings cmd_bindings["previous_iframe"] = None cmd_bindings["next_iframe"] = None cmd_bindings["list_iframes"] = None commands_data = [ ("previous_blockquote", self.previous_blockquote, cmdnames.BLOCKQUOTE_PREV), ("next_blockquote", self.next_blockquote, cmdnames.BLOCKQUOTE_NEXT), ("list_blockquotes", self.list_blockquotes, cmdnames.BLOCKQUOTE_LIST), ("previous_button", self.previous_button, cmdnames.BUTTON_PREV), ("next_button", self.next_button, cmdnames.BUTTON_NEXT), ("list_buttons", self.list_buttons, cmdnames.BUTTON_LIST), ("previous_checkbox", self.previous_checkbox, cmdnames.CHECK_BOX_PREV), ("next_checkbox", self.next_checkbox, cmdnames.CHECK_BOX_NEXT), ("list_checkboxes", self.list_checkboxes, cmdnames.CHECK_BOX_LIST), ("previous_combobox", self.previous_combobox, cmdnames.COMBO_BOX_PREV), ("next_combobox", self.next_combobox, cmdnames.COMBO_BOX_NEXT), ("list_comboboxes", self.list_comboboxes, cmdnames.COMBO_BOX_LIST), ("previous_entry", self.previous_entry, cmdnames.ENTRY_PREV), ("next_entry", self.next_entry, cmdnames.ENTRY_NEXT), ("list_entries", self.list_entries, cmdnames.ENTRY_LIST), ("previous_form_field", self.previous_form_field, cmdnames.FORM_FIELD_PREV), ("next_form_field", self.next_form_field, cmdnames.FORM_FIELD_NEXT), ("list_form_fields", self.list_form_fields, cmdnames.FORM_FIELD_LIST), ("previous_heading", self.previous_heading, cmdnames.HEADING_PREV), ("next_heading", self.next_heading, cmdnames.HEADING_NEXT), ("list_headings", self.list_headings, cmdnames.HEADING_LIST), ("previous_iframe", self.previous_iframe, cmdnames.IFRAME_PREV), ("next_iframe", self.next_iframe, cmdnames.IFRAME_NEXT), ("list_iframes", self.list_iframes, cmdnames.IFRAME_LIST), ("previous_image", self.previous_image, cmdnames.IMAGE_PREV), ("next_image", self.next_image, cmdnames.IMAGE_NEXT), ("list_images", self.list_images, cmdnames.IMAGE_LIST), ("previous_landmark", self.previous_landmark, cmdnames.LANDMARK_PREV), ("next_landmark", self.next_landmark, cmdnames.LANDMARK_NEXT), ("list_landmarks", self.list_landmarks, cmdnames.LANDMARK_LIST), ("previous_list", self.previous_list, cmdnames.LIST_PREV), ("next_list", self.next_list, cmdnames.LIST_NEXT), ("list_lists", self.list_lists, cmdnames.LIST_LIST), ("previous_list_item", self.previous_list_item, cmdnames.LIST_ITEM_PREV), ("next_list_item", self.next_list_item, cmdnames.LIST_ITEM_NEXT), ("list_list_items", self.list_list_items, cmdnames.LIST_ITEM_LIST), ("previous_live_region", self.previous_live_region, cmdnames.LIVE_REGION_PREV), ("next_live_region", self.next_live_region, cmdnames.LIVE_REGION_NEXT), ("last_live_region", self._last_live_region, cmdnames.LIVE_REGION_LAST), ("previous_paragraph", self.previous_paragraph, cmdnames.PARAGRAPH_PREV), ("next_paragraph", self.next_paragraph, cmdnames.PARAGRAPH_NEXT), ("list_paragraphs", self.list_paragraphs, cmdnames.PARAGRAPH_LIST), ("previous_radio_button", self.previous_radio_button, cmdnames.RADIO_BUTTON_PREV), ("next_radio_button", self.next_radio_button, cmdnames.RADIO_BUTTON_NEXT), ("list_radio_buttons", self.list_radio_buttons, cmdnames.RADIO_BUTTON_LIST), ("previous_separator", self.previous_separator, cmdnames.SEPARATOR_PREV), ("next_separator", self.next_separator, cmdnames.SEPARATOR_NEXT), ("previous_table", self.previous_table, cmdnames.TABLE_PREV), ("next_table", self.next_table, cmdnames.TABLE_NEXT), ("list_tables", self.list_tables, cmdnames.TABLE_LIST), ("previous_link", self.previous_link, cmdnames.LINK_PREV), ("next_link", self.next_link, cmdnames.LINK_NEXT), ("list_links", self.list_links, cmdnames.LINK_LIST), ("previous_unvisited_link", self.previous_unvisited_link, cmdnames.UNVISITED_LINK_PREV), ("next_unvisited_link", self.next_unvisited_link, cmdnames.UNVISITED_LINK_NEXT), ("list_unvisited_links", self.list_unvisited_links, cmdnames.UNVISITED_LINK_LIST), ("previous_visited_link", self.previous_visited_link, cmdnames.VISITED_LINK_PREV), ("next_visited_link", self.next_visited_link, cmdnames.VISITED_LINK_NEXT), ("list_visited_links", self.list_visited_links, cmdnames.VISITED_LINK_LIST), ("previous_large_object", self.previous_large_object, cmdnames.LARGE_OBJECT_PREV), ("next_large_object", self.next_large_object, cmdnames.LARGE_OBJECT_NEXT), ("list_large_objects", self.list_large_objects, cmdnames.LARGE_OBJECT_LIST), ("previous_clickable", self.previous_clickable, cmdnames.CLICKABLE_PREV), ("next_clickable", self.next_clickable, cmdnames.CLICKABLE_NEXT), ("list_clickables", self.list_clickables, cmdnames.CLICKABLE_LIST), ("container_start", self.container_start, cmdnames.CONTAINER_START), ("container_end", self.container_end, cmdnames.CONTAINER_END), ] for name, function, description in commands_data: kb = cmd_bindings.get(name) manager.add_command( command_manager.KeyboardCommand( name, function, group_label, description, desktop_keybinding=kb, laptop_keybinding=kb, ), ) # Heading levels 1-6 for i in range(1, 7): kb_prev = keybindings.KeyBinding(str(i), keybindings.SHIFT_MODIFIER_MASK) kb_next = keybindings.KeyBinding(str(i), keybindings.NO_MODIFIER_MASK) kb_list = keybindings.KeyBinding(str(i), keybindings.SHIFT_ALT_MODIFIER_MASK) heading_commands = [ ( f"previous_heading_level_{i}", getattr(self, f"previous_heading_level_{i}"), cmdnames.HEADING_AT_LEVEL_PREV % i, kb_prev, ), ( f"next_heading_level_{i}", getattr(self, f"next_heading_level_{i}"), cmdnames.HEADING_AT_LEVEL_NEXT % i, kb_next, ), ( f"list_headings_level_{i}", getattr(self, f"list_headings_level_{i}"), cmdnames.HEADING_AT_LEVEL_LIST % i, kb_list, ), ] for name, function, description, kb in heading_commands: manager.add_command( command_manager.KeyboardCommand( name, function, group_label, description, desktop_keybinding=kb, laptop_keybinding=kb, ), ) msg = f"STRUCTURAL NAVIGATOR: Commands set up. Suspended: {self._suspended}" debug.print_message(debug.LEVEL_INFO, msg, True) def _is_active_script(self, script): active_script = script_manager.get_manager().get_active_script() if active_script == script: return True tokens = ["STRUCTURAL NAVIGATOR:", script, "is not the active script", active_script] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return False def get_mode(self, script: default.Script) -> NavigationMode: """Returns the current structural-navigator mode associated with script.""" mode = self._mode_for_script.get(script, NavigationMode.OFF) tokens = ["STRUCTURAL NAVIGATOR: Mode for", script, f"is {mode}"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return mode def set_mode(self, script: default.Script, mode: NavigationMode) -> None: """Sets the structural-navigator mode.""" tokens = ["STRUCTURAL NAVIGATOR: Setting mode for", script, f"to {mode}"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._mode_for_script[script] = mode if not (script and self._is_active_script(script)): return # Use the per-script mode combined with the user's preference to determine # whether commands should be active, without overwriting the preference. effective = mode != NavigationMode.OFF and self.get_is_enabled() command_manager.get_manager().set_group_enabled( guilabels.KB_GROUP_STRUCTURAL_NAVIGATION, effective, ) def last_input_event_was_navigation_command(self) -> bool: """Returns true if the last input event was a navigation command.""" if self._last_input_event is None: return False manager = input_event_manager.get_manager() result = manager.last_event_equals_or_is_release_for_event(self._last_input_event) if self._last_input_event is not None: string = self._last_input_event.as_single_line_string() else: string = "None" msg = ( f"STRUCTURAL NAVIGATOR: Last navigation event ({string}) is last input event: {result}" ) debug.print_message(debug.LEVEL_INFO, msg, True) return result def last_command_prevents_focus_mode(self) -> bool: """Returns True if the last command was navigation but the setting disallows focus mode.""" if not self.last_input_event_was_navigation_command(): return False return not self.get_triggers_focus_mode() @gsettings_registry.get_registry().gsetting( key=KEY_WRAPS, schema="structural-navigation", gtype="b", default=True, summary="Wrap when reaching top/bottom", migration_key="wrappedStructuralNavigation", ) @dbus_service.getter def get_navigation_wraps(self) -> bool: """Returns whether navigation wraps when reaching the top/bottom of the document.""" return self._get_setting(self.KEY_WRAPS, "b", True) @dbus_service.setter def set_navigation_wraps(self, value: bool) -> bool: """Sets whether navigation wraps when reaching the top/bottom of the document.""" msg = f"STRUCTURAL NAVIGATOR: Setting navigation wraps to {value}." debug.print_message(debug.LEVEL_INFO, msg, True) gsettings_registry.get_registry().set_runtime_value(self._SCHEMA, self.KEY_WRAPS, value) return True @gsettings_registry.get_registry().gsetting( key=KEY_LARGE_OBJECT_TEXT_LENGTH, schema="structural-navigation", gtype="i", default=75, summary="Minimum text length for large objects", migration_key="largeObjectTextLength", ) @dbus_service.getter def get_large_object_text_length(self) -> int: """Returns the minimum number of characters to be considered a 'large object'.""" return self._get_setting(self.KEY_LARGE_OBJECT_TEXT_LENGTH, "i", 75) @dbus_service.setter def set_large_object_text_length(self, value: int) -> bool: """Sets the minimum number of characters to be considered a 'large object'.""" msg = f"STRUCTURAL NAVIGATOR: Setting large object text length to {value}." debug.print_message(debug.LEVEL_INFO, msg, True) gsettings_registry.get_registry().set_runtime_value( self._SCHEMA, self.KEY_LARGE_OBJECT_TEXT_LENGTH, value, ) return True @gsettings_registry.get_registry().gsetting( key=KEY_ENABLED, schema="structural-navigation", gtype="b", default=True, summary="Enable structural navigation", migration_key="structuralNavigationEnabled", ) @dbus_service.getter def get_is_enabled(self) -> bool: """Returns whether structural navigation is enabled.""" return self._get_setting(self.KEY_ENABLED, "b", True) @dbus_service.setter def set_is_enabled(self, value: bool) -> bool: """Sets whether structural navigation is enabled.""" if self.get_is_enabled() == value: msg = f"STRUCTURAL NAVIGATOR: Enabled already {value}. Refreshing command group." debug.print_message(debug.LEVEL_INFO, msg, True) command_manager.get_manager().set_group_enabled( guilabels.KB_GROUP_STRUCTURAL_NAVIGATION, value, ) return True msg = f"STRUCTURAL NAVIGATOR: Setting enabled to {value}." debug.print_message(debug.LEVEL_INFO, msg, True) gsettings_registry.get_registry().set_runtime_value( self._SCHEMA, self.KEY_ENABLED, value, ) script = script_manager.get_manager().get_active_script() if not script: return True current_mode = self.get_mode(script) if not value and current_mode == NavigationMode.OFF: return True self._last_input_event = None if value: if previous_mode := self._previous_mode_for_script.get(script): tokens = ["STRUCTURAL NAVIGATOR: Restoring mode for", script, "to", previous_mode] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._mode_for_script[script] = previous_mode else: self._previous_mode_for_script[script] = current_mode tokens = ["STRUCTURAL NAVIGATOR: Saving", current_mode, "as previous mode for", script] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._mode_for_script[script] = NavigationMode.OFF command_manager.get_manager().set_group_enabled( guilabels.KB_GROUP_STRUCTURAL_NAVIGATION, value, ) return True @gsettings_registry.get_registry().gsetting( key=KEY_TRIGGERS_FOCUS_MODE, schema="structural-navigation", gtype="b", default=False, summary="Structural navigation triggers focus mode", migration_key="structNavTriggersFocusMode", ) @dbus_service.getter def get_triggers_focus_mode(self) -> bool: """Returns whether structural navigation triggers focus mode.""" return self._get_setting(self.KEY_TRIGGERS_FOCUS_MODE, "b", False) @dbus_service.setter def set_triggers_focus_mode(self, value: bool) -> bool: """Sets whether structural navigation triggers focus mode.""" if self.get_triggers_focus_mode() == value: return True msg = f"STRUCTURAL NAVIGATOR: Setting triggers focus mode to {value}." debug.print_message(debug.LEVEL_INFO, msg, True) gsettings_registry.get_registry().set_runtime_value( self._SCHEMA, self.KEY_TRIGGERS_FOCUS_MODE, value, ) return True @dbus_service.command def cycle_mode( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Cycles among the structural navigation modes.""" tokens = [ "STRUCTURAL NAVIGATOR: cycle_mode. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not (script and self._is_active_script(script)): return False self._last_input_event = None previous_mode = self.get_mode(script) msg = "" mode = None if previous_mode == NavigationMode.OFF: mode = NavigationMode.DOCUMENT msg = messages.STRUCTURAL_NAVIGATION_KEYS_DOCUMENT elif previous_mode == NavigationMode.GUI: mode = NavigationMode.OFF msg = messages.STRUCTURAL_NAVIGATION_KEYS_OFF else: mode = NavigationMode.GUI msg = messages.STRUCTURAL_NAVIGATION_KEYS_GUI if notify_user: presentation_manager.get_manager().present_message(msg) self.set_mode(script, mode) if mode == NavigationMode.DOCUMENT: root = self._determine_root_container(script) if not AXObject.supports_collection(root) and notify_user: presentation_manager.get_manager().present_message( messages.STRUCTURAL_NAVIGATION_NOT_SUPPORTED_FULL, messages.STRUCTURAL_NAVIGATION_NOT_SUPPORTED_BRIEF, ) return True def suspend_commands(self, script, suspended, reason=""): """Suspends structural navigation independent of the enabled setting.""" if not (script and self._is_active_script(script)): return msg = f"STRUCTURAL NAVIGATOR: Suspended: {suspended}" if reason: msg += f": {reason}" debug.print_message(debug.LEVEL_INFO, msg, True) self._suspended = suspended command_manager.get_manager().set_group_suspended( guilabels.KB_GROUP_STRUCTURAL_NAVIGATION, suspended, ) def _get_container_for_nested_item(self, obj: Atspi.Accessible) -> Atspi.Accessible: # If an author put an ARIA heading inside a native heading (or vice versa), obj # could be the inner heading. If we treat the outer heading as as the previous heading # and then set the caret context to the first position inside the outer heading, i.e. # the inner heading, we'll get stuck. Thanks authors. if AXUtilities.is_heading(obj): if ancestor := AXUtilities.find_ancestor(obj, AXUtilities.is_heading): tokens = [ "STRUCTURAL NAVIGATOR: Current heading", obj, "is inside another heading", ancestor, "Treating the outer heading as current.", ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return ancestor return obj candidate = obj if AXUtilities.is_live_region(obj): while ancestor := AXUtilities.find_ancestor(candidate, AXUtilities.is_live_region): candidate = ancestor if candidate != obj: tokens = [ "STRUCTURAL NAVIGATOR: Current live region", obj, "is inside another ", "live region", candidate, "Treating the outer region as current.", ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return candidate @staticmethod def _get_adjacent_or_wrap( objects: list[Atspi.Accessible], index: int, is_next: bool, should_wrap: bool, notify_user: bool, ) -> Atspi.Accessible | None: """Returns the adjacent object in the list, wrapping if enabled.""" if is_next: if index + 1 < len(objects): return objects[index + 1] wrap_msg = messages.WRAPPING_TO_TOP wrap_target = objects[0] else: if index > 0: return objects[index - 1] wrap_msg = messages.WRAPPING_TO_BOTTOM wrap_target = objects[-1] if not should_wrap: return None if notify_user: presentation_manager.get_manager().present_message(wrap_msg) return wrap_target def _get_object_in_direction( self, script: default.Script, objects: list[Atspi.Accessible], is_next: bool, should_wrap: bool | None = None, notify_user: bool = True, ) -> Atspi.Accessible | None: """Returns the next/previous object in relation to the current location.""" if not objects: return None if should_wrap is None: should_wrap = self.get_navigation_wraps() # If we're in a matching object, return the next/previous one in the list. obj = focus_manager.get_manager().get_locus_of_focus() candidate = obj while candidate: if candidate not in objects: candidate = AXObject.get_parent(candidate) continue if not is_next: alternative = self._get_container_for_nested_item(candidate) if alternative in objects: candidate = alternative index = objects.index(candidate) return self._get_adjacent_or_wrap( objects, index, is_next, should_wrap, notify_user, ) # If we're not in a matching object, find the next/previous one based on the path. if not is_next: objects.reverse() current_path = AXObject.get_path(obj) for match in objects: path = AXObject.get_path(match) comparison = script.utilities.path_comparison(path, current_path) if (comparison > 0 and is_next) or (comparison < 0 and not is_next): return match if not should_wrap: return None wrap_msg = messages.WRAPPING_TO_TOP if is_next else messages.WRAPPING_TO_BOTTOM if notify_user: presentation_manager.get_manager().present_message(wrap_msg) return objects[0] if obj != objects[0] else None def _get_state_string(self, obj: Atspi.Accessible) -> str: if AXUtilities.is_switch(obj): off, on = object_properties.SWITCH_INDICATORS_SPEECH return on if AXUtilities.is_checked(obj) else off if AXUtilities.is_check_box(obj): unchecked, checked, partially = object_properties.CHECK_BOX_INDICATORS_SPEECH if AXUtilities.is_indeterminate(obj): return partially return checked if AXUtilities.is_checked(obj) else unchecked if AXUtilities.is_radio_button(obj): unselected, selected = object_properties.RADIO_BUTTON_INDICATORS_SPEECH return selected if AXUtilities.is_checked(obj) else unselected if AXUtilities.is_link(obj): return ( object_properties.STATE_VISITED if AXUtilities.is_visited(obj) else object_properties.STATE_UNVISITED ) return "" def _get_item_string_by_role( self, script: default.Script, obj: Atspi.Accessible, ) -> str | None: """Returns a string for the object based on its role, or None if not role-specific.""" if AXUtilities.is_table(obj): caption = AXTable.get_caption(obj) return AXText.get_all_text(caption) if caption else "" if AXUtilities.is_internal_frame(obj): result = self._get_item_string(script, AXObject.get_child(obj, 0)) return result or AXUtilities.get_localized_role_name(obj) if AXUtilities.is_list(obj): children = list(AXObject.iter_children(obj, AXUtilities.is_list_item)) count = len(children) counter = ( messages.nested_list_item_count if AXUtilities.get_nesting_level(obj) else messages.list_item_count ) return counter(count) if AXUtilities.is_description_list(obj): return messages.description_list_term_count( len(AXUtilities.find_all_description_terms(obj)), ) if AXUtilities.is_image(obj): result = AXObject.get_image_description(obj) if not result: parent = AXObject.get_parent(obj) if AXUtilities.is_link(parent): result = self._get_item_string(script, parent) else: result = AXUtilities.get_localized_role_name(obj) return result return None def _get_item_string(self, script: default.Script, obj: Atspi.Accessible) -> str: if obj is None: return "" result = ( AXObject.get_name(obj) or AXObject.get_description(obj) or AXUtilities.get_displayed_label(obj) or AXUtilities.get_displayed_description(obj) ) if result: return result role_result = self._get_item_string_by_role(script, obj) if role_result is not None: return role_result if AXUtilities.is_page_tab_list(obj): return messages.tab_list_item_count( len(list(AXObject.iter_children(obj, AXUtilities.is_page_tab))), ) if result := script.utilities.expand_eocs(obj): return result if AXUtilities.is_link(obj): result = AXHypertext.get_link_basename(obj) return result def _present_line( self, script: default.Script, obj: Atspi.Accessible | None = None, offset: int | None = None, notify_user: bool = True, ) -> None: if obj is None: return manager = focus_manager.get_manager() presenter = say_all_presenter.get_presenter() if manager.in_say_all() and presenter.get_structural_navigation_enabled(): presenter.say_all(script, event=None, obj=obj, offset=offset) return manager.emit_region_changed(obj, offset, mode=focus_manager.STRUCTURAL_NAVIGATOR) if not notify_user: msg = "STRUCTURAL NAVIGATOR: _present_line called with notify_user=False" debug.print_message(debug.LEVEL_INFO, msg, True) manager.set_locus_of_focus(None, obj, False) if AXObject.supports_text(obj): script.utilities.set_caret_position(obj, offset or 0) return script.update_braille(obj) script.say_line(obj, offset) def _present_object( self, script: default.Script, obj: Atspi.Accessible | None = None, not_found_message: str = messages.STRUCTURAL_NAVIGATION_NOT_FOUND, offset: int | None = None, notify_user: bool = True, ) -> None: if obj is None: if notify_user: presentation_manager.get_manager().present_message( not_found_message, messages.STRUCTURAL_NAVIGATION_NOT_FOUND, ) return if offset is None: offset = 0 manager = focus_manager.get_manager() if self.get_mode(script) == NavigationMode.GUI: manager.set_locus_of_focus(None, obj) AXObject.grab_focus(obj) AXObject.clear_cache(obj, False, "Checking state after focus grab") if not AXUtilities.is_focused(obj) and notify_user: presentation_manager.get_manager().present_message(messages.NOT_FOCUSED) return presenter = say_all_presenter.get_presenter() if manager.in_say_all() and presenter.get_structural_navigation_enabled(): presenter.say_all(script, event=None, obj=obj, offset=offset) return manager.emit_region_changed(obj, offset, mode=focus_manager.STRUCTURAL_NAVIGATOR) if not notify_user: msg = "STRUCTURAL NAVIGATOR: _present_object called with notify_user=False" debug.print_message(debug.LEVEL_INFO, msg, True) manager.set_locus_of_focus(None, obj, False) if AXObject.supports_text(obj): script.utilities.set_caret_position(obj, offset) return script.present_object(obj, offset=offset, interrupt=True) def _present_object_list( self, script: default.Script, objects: list[Atspi.Accessible], dialog_title: str, column_headers: list[str], row_data_func: Callable, notify_user: bool = True, ) -> None: dialog_title = f"{dialog_title}: {messages.items_found(len(objects))}" if not objects: if notify_user: presentation_manager.get_manager().present_message(dialog_title) return current_object = script.utilities.get_caret_context()[0] try: index = objects.index(current_object) except ValueError: index = 0 rows = [(obj, -1, *row_data_func(obj)) for obj in objects] orca_gui_navlist.show_ui(dialog_title, column_headers, rows, index) def _determine_root_container(self, script: default.Script) -> Atspi.Accessible: mode = self.get_mode(script) focus = focus_manager.get_manager().get_locus_of_focus() root = AXUtilities.find_ancestor_inclusive(focus, AXUtilities.is_modal_dialog) if root is None: if mode == NavigationMode.DOCUMENT: root = script.utilities.get_top_level_document_for_object(focus) elif mode == NavigationMode.GUI: root = AXUtilities.find_ancestor_inclusive(focus, AXUtilities.is_dialog_or_window) if root is None: root = focus_manager.get_manager().get_active_window() tokens = ["STRUCTURAL NAVIGATOR: Root for", focus, "is", root, f"mode: {mode}"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return root def _is_non_document_object(self, obj: Atspi.Accessible, must_be_showing: bool = True) -> bool: if AXUtilities.find_ancestor_inclusive(obj, AXUtilities.is_document) is not None: return False return not (must_be_showing and not AXUtilities.is_showing(obj)) ######################## # # # Blockquotes # # # ######################## def _get_all_blockquotes(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_block_quotes(root, pred=pred) @dbus_service.command def previous_blockquote( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous blockquote.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_blockquote. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_blockquotes(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_BLOCKQUOTES, notify_user=notify_user) return True @dbus_service.command def next_blockquote( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next blockquote.""" tokens = [ "STRUCTURAL NAVIGATOR: next_blockquote. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_blockquotes(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_BLOCKQUOTES, notify_user=notify_user) return True @dbus_service.command def list_blockquotes( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of blockquotes.""" tokens = [ "STRUCTURAL NAVIGATOR: list_blockquotes. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_blockquotes(script), guilabels.SN_TITLE_BLOCKQUOTE, [guilabels.SN_HEADER_BLOCKQUOTE], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # Buttons # # # ######################## def _get_all_buttons(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_buttons(root, pred=pred) @dbus_service.command def previous_button( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous button.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_button. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_buttons(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_BUTTONS, notify_user=notify_user) return True @dbus_service.command def next_button( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next button.""" tokens = [ "STRUCTURAL NAVIGATOR: next_button. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_buttons(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_BUTTONS, notify_user=notify_user) return True @dbus_service.command def list_buttons( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of buttons.""" tokens = [ "STRUCTURAL NAVIGATOR: list_buttons. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_buttons(script), guilabels.SN_TITLE_BUTTON, [guilabels.SN_HEADER_BUTTON], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # Check boxes # # # ######################## def _get_all_checkboxes(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_check_boxes(root, pred=pred) @dbus_service.command def previous_checkbox( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous checkbox.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_checkbox. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_checkboxes(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_CHECK_BOXES, notify_user=notify_user) return True @dbus_service.command def next_checkbox( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next checkbox.""" tokens = [ "STRUCTURAL NAVIGATOR: next_checkbox. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_checkboxes(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_CHECK_BOXES, notify_user=notify_user) return True @dbus_service.command def list_checkboxes( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of checkboxes.""" tokens = [ "STRUCTURAL NAVIGATOR: list_checkboxes. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_checkboxes(script), guilabels.SN_TITLE_CHECK_BOX, [guilabels.SN_HEADER_CHECK_BOX, guilabels.SN_HEADER_STATE], lambda obj: [self._get_item_string(script, obj), self._get_state_string(obj)], notify_user=notify_user, ) return True ######################## # # # Large Objects # # # ######################## def _get_all_large_objects(self, script: default.Script) -> list[Atspi.Accessible]: minimum_length = self.get_large_object_text_length() def _is_large(obj): if AXUtilities.is_heading(obj): return True if AXUtilities.is_list(obj): return True if AXUtilities.is_table(obj): return True text = AXText.get_all_text(obj) return len(text) > minimum_length and text.count("\ufffc") / len(text) < 0.05 root = self._determine_root_container(script) roles = [ *AXUtilities.get_large_container_roles(), Atspi.Role.HEADING, Atspi.Role.PARAGRAPH, Atspi.Role.SECTION, ] return AXUtilities.find_all_with_role(root, roles, pred=_is_large) @dbus_service.command def previous_large_object( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous large object.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_large_object. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_large_objects(script) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_LARGE_OBJECTS, notify_user=notify_user, ) return True @dbus_service.command def next_large_object( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next large object.""" tokens = [ "STRUCTURAL NAVIGATOR: next_large_object. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_large_objects(script) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_LARGE_OBJECTS, notify_user=notify_user, ) return True @dbus_service.command def list_large_objects( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of large objects.""" tokens = ["STRUCTURAL NAVIGATOR: list_large_objects. Script:", script, "Event:", event] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_large_objects(script), guilabels.SN_TITLE_LARGE_OBJECT, [guilabels.SN_HEADER_OBJECT, guilabels.SN_HEADER_ROLE], lambda obj: [ self._get_item_string(script, obj), AXUtilities.get_localized_role_name(obj), ], notify_user=notify_user, ) return True ######################## # # # Combo Boxes # # # ######################## def _get_all_comboboxes(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_combo_boxes(root, pred=pred) @dbus_service.command def previous_combobox( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous combo box.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_combobox. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_comboboxes(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_COMBO_BOXES, notify_user=notify_user) return True @dbus_service.command def next_combobox( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next combo box.""" tokens = [ "STRUCTURAL NAVIGATOR: next_combobox. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_comboboxes(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_COMBO_BOXES, notify_user=notify_user) return True @dbus_service.command def list_comboboxes( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of combo boxes.""" tokens = [ "STRUCTURAL NAVIGATOR: list_comboboxes. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_comboboxes(script), guilabels.SN_TITLE_COMBO_BOX, [guilabels.SN_HEADER_COMBO_BOX], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # Entries # # # ######################## def _get_all_entries(self, script: default.Script) -> list[Atspi.Accessible]: def parent_is_not_editable(obj): parent = AXObject.get_parent(obj) return parent is not None and not AXUtilities.is_editable(parent) if self.get_mode(script) == NavigationMode.GUI: def pred(x): return self._is_non_document_object(x) else: pred = parent_is_not_editable root = self._determine_root_container(script) return AXUtilities.find_all_editable_objects(root, pred=pred) @dbus_service.command def previous_entry( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous entry.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_entry. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_entries(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_ENTRIES, notify_user=notify_user) return True @dbus_service.command def next_entry( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next entry.""" tokens = [ "STRUCTURAL NAVIGATOR: next_entry. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_entries(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_ENTRIES, notify_user=notify_user) return True @dbus_service.command def list_entries( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of entries.""" tokens = [ "STRUCTURAL NAVIGATOR: list_entries. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_entries(script), guilabels.SN_TITLE_ENTRY, [guilabels.SN_HEADER_LABEL, guilabels.SN_HEADER_VALUE], lambda obj: [self._get_item_string(script, obj), AXText.get_all_text(obj)], notify_user=notify_user, ) return True ######################## # # # Form Fields # # # ######################## def _get_all_form_fields(self, script: default.Script) -> list[Atspi.Accessible]: def is_not_noneditable_doc_frame(obj): if AXUtilities.is_document_frame(obj): return AXUtilities.is_editable(obj) return True def pred(x): if self.get_mode(script) == NavigationMode.GUI: return self._is_non_document_object(x) return is_not_noneditable_doc_frame(x) root = self._determine_root_container(script) return AXUtilities.find_all_form_fields(root, pred=pred) @dbus_service.command def previous_form_field( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous form field.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_form_field. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_form_fields(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_FORM_FIELDS, notify_user=notify_user) return True @dbus_service.command def next_form_field( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next form field.""" tokens = [ "STRUCTURAL NAVIGATOR: next_form_field. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_form_fields(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_FORM_FIELDS, notify_user=notify_user) return True @dbus_service.command def list_form_fields( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of form fields.""" tokens = [ "STRUCTURAL NAVIGATOR: list_form_fields. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_form_fields(script), guilabels.SN_TITLE_FORM_FIELD, [guilabels.SN_HEADER_LABEL, guilabels.SN_HEADER_ROLE, guilabels.SN_HEADER_VALUE], lambda obj: [ self._get_item_string(script, obj), AXUtilities.get_localized_role_name(obj), AXText.get_all_text(obj), ], notify_user=notify_user, ) return True ######################## # # # Headings # # # ######################## def _get_all_headings( self, script: default.Script, level: int | None = None, ) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) if level is None: return AXUtilities.find_all_headings(root, pred=pred) return AXUtilities.find_all_headings_at_level(root, level, pred=pred) @dbus_service.command def previous_heading( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous heading.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_heading. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_HEADINGS, notify_user=notify_user) return True @dbus_service.command def next_heading( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next heading.""" tokens = [ "STRUCTURAL NAVIGATOR: next_heading. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_HEADINGS, notify_user=notify_user) return True @dbus_service.command def list_headings( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of headings.""" tokens = [ "STRUCTURAL NAVIGATOR: list_headings. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_headings(script), guilabels.SN_TITLE_HEADING, [guilabels.SN_HEADER_HEADING, guilabels.SN_HEADER_LEVEL], lambda obj: [ self._get_item_string(script, obj), str(AXUtilities.get_heading_level(obj)), ], notify_user=notify_user, ) return True @dbus_service.command def previous_heading_level_1( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous level 1 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_heading_level_1. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 1) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 1, notify_user=notify_user, ) return True @dbus_service.command def next_heading_level_1( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next level 1 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: next_heading_level_1. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 1) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 1, notify_user=notify_user, ) return True @dbus_service.command def list_headings_level_1( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of level 1 headings.""" tokens = [ "STRUCTURAL NAVIGATOR: list_headings_level_1. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_headings(script), guilabels.SN_TITLE_HEADING_AT_LEVEL % 1, [guilabels.SN_HEADER_HEADING], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True @dbus_service.command def previous_heading_level_2( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous level 2 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_heading_level_2. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 2) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 2, notify_user=notify_user, ) return True @dbus_service.command def next_heading_level_2( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next level 2 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: next_heading_level_2. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 2) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 2, notify_user=notify_user, ) return True @dbus_service.command def list_headings_level_2( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of level 2 headings.""" tokens = [ "STRUCTURAL NAVIGATOR: list_headings_level_2. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_headings(script), guilabels.SN_TITLE_HEADING_AT_LEVEL % 2, [guilabels.SN_HEADER_HEADING], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True @dbus_service.command def previous_heading_level_3( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous level 3 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_heading_level_3. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 3) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 3, notify_user=notify_user, ) return True @dbus_service.command def next_heading_level_3( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next level 3 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: next_heading_level_3. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 3) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 3, notify_user=notify_user, ) return True @dbus_service.command def list_headings_level_3( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of level 3 headings.""" tokens = [ "STRUCTURAL NAVIGATOR: list_headings_level_3. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_headings(script), guilabels.SN_TITLE_HEADING_AT_LEVEL % 3, [guilabels.SN_HEADER_HEADING], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True @dbus_service.command def previous_heading_level_4( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous level 4 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_heading_level_4. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 4) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 4, notify_user=notify_user, ) return True @dbus_service.command def next_heading_level_4( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next level 4 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: next_heading_level_4. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 4) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 4, notify_user=notify_user, ) return True @dbus_service.command def list_headings_level_4( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of level 4 headings.""" tokens = [ "STRUCTURAL NAVIGATOR: list_headings_level_4. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_headings(script), guilabels.SN_TITLE_HEADING_AT_LEVEL % 4, [guilabels.SN_HEADER_HEADING], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True @dbus_service.command def previous_heading_level_5( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous level 5 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_heading_level_5. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 5) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 5, notify_user=notify_user, ) return True @dbus_service.command def next_heading_level_5( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next level 5 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: next_heading_level_5. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 5) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 5, notify_user=notify_user, ) return True @dbus_service.command def list_headings_level_5( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of level 5 headings.""" tokens = [ "STRUCTURAL NAVIGATOR: list_headings_level_5. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_headings(script), guilabels.SN_TITLE_HEADING_AT_LEVEL % 5, [guilabels.SN_HEADER_HEADING], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True @dbus_service.command def previous_heading_level_6( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous level 6 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_heading_level_6. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 6) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 6, notify_user=notify_user, ) return True @dbus_service.command def next_heading_level_6( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next level 6 heading.""" tokens = [ "STRUCTURAL NAVIGATOR: next_heading_level_6. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_headings(script, 6) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_HEADINGS_AT_LEVEL % 6, notify_user=notify_user, ) return True @dbus_service.command def list_headings_level_6( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of level 6 headings.""" tokens = [ "STRUCTURAL NAVIGATOR: list_headings_level_6. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_headings(script), guilabels.SN_TITLE_HEADING_AT_LEVEL % 6, [guilabels.SN_HEADER_HEADING], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # Iframes # # # ######################## def _get_all_iframes(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_internal_frames(root, pred=pred) @dbus_service.command def previous_iframe( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous iframe.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_iframe. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_iframes(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_IFRAMES, notify_user=notify_user) return True @dbus_service.command def next_iframe( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next iframe.""" tokens = [ "STRUCTURAL NAVIGATOR: next_iframe. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_iframes(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_IFRAMES, notify_user=notify_user) return True @dbus_service.command def list_iframes( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of iframes.""" tokens = [ "STRUCTURAL NAVIGATOR: list_iframes. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_iframes(script), guilabels.SN_TITLE_IFRAME, [guilabels.SN_HEADER_IFRAME], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # Images # # # ######################## def _get_all_images(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_images_and_image_maps(root, pred=pred) @dbus_service.command def previous_image( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous image.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_image. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_images(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_IMAGES, notify_user=notify_user) return True @dbus_service.command def next_image( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next image.""" tokens = [ "STRUCTURAL NAVIGATOR: next_image. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_images(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_IMAGES, notify_user=notify_user) return True @dbus_service.command def list_images( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of images.""" tokens = [ "STRUCTURAL NAVIGATOR: list_images. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_images(script), guilabels.SN_TITLE_IMAGE, [guilabels.SN_HEADER_IMAGE], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # Landmarks # # # ######################## def _get_all_landmarks(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_landmarks(root, pred=pred) def _present_landmark( self, script: default.Script, obj: Atspi.Accessible, notify_user: bool, ) -> None: if obj is None: self._present_object(script, obj, messages.NO_LANDMARK_FOUND, notify_user=notify_user) return if notify_user: presentation_manager.get_manager().present_message(AXObject.get_name(obj)) self._present_line(script, obj, 0) @dbus_service.command def previous_landmark( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous landmark.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_landmark. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_landmarks(script) result = self._get_object_in_direction(script, matches, False) self._present_landmark(script, result, notify_user) return True @dbus_service.command def next_landmark( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next landmark.""" tokens = [ "STRUCTURAL NAVIGATOR: next_landmark. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_landmarks(script) result = self._get_object_in_direction(script, matches, True) self._present_landmark(script, result, notify_user) return True @dbus_service.command def list_landmarks( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of landmarks.""" tokens = [ "STRUCTURAL NAVIGATOR: list_landmarks. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_landmarks(script), guilabels.SN_TITLE_LANDMARK, [guilabels.SN_HEADER_LANDMARK, guilabels.SN_HEADER_ROLE], lambda obj: [ self._get_item_string(script, obj), AXUtilities.get_localized_role_name(obj), ], notify_user=notify_user, ) return True ######################## # # # Lists # # # ######################## def _get_all_lists(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_lists( root, include_description_lists=True, include_tab_lists=True, pred=pred, ) def _get_first_item(self, obj: Atspi.Accessible) -> Atspi.Accessible | None: # The reason we present the item (or first child) rather than the full list are twofold: # 1. Given a huge list, navigating to the item and presenting the ancestor list is more # performant. # 2. When we calculate what's on the same line, it should be based on the item's bounding # box; not the list's. # TODO - JD: Handle the second issue in the utilities which calculate the line. return AXObject.get_child(obj, 0) @dbus_service.command def previous_list( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous list.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_list. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_lists(script) result = self._get_object_in_direction(script, matches, False) result = self._get_first_item(result) or result self._present_object(script, result, messages.NO_MORE_LISTS, notify_user=notify_user) return True @dbus_service.command def next_list( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next list.""" tokens = [ "STRUCTURAL NAVIGATOR: next_list. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_lists(script) result = self._get_object_in_direction(script, matches, True) result = self._get_first_item(result) or result self._present_object(script, result, messages.NO_MORE_LISTS, notify_user=notify_user) return True @dbus_service.command def list_lists( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of lists.""" tokens = [ "STRUCTURAL NAVIGATOR: list_lists. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_lists(script), guilabels.SN_TITLE_LIST, [guilabels.SN_HEADER_LIST], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # List Items # # # ######################## def _get_all_list_items(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_list_items( root, include_description_terms=True, include_tabs=True, pred=pred, ) @dbus_service.command def previous_list_item( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous list item.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_list_item. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_list_items(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_LIST_ITEMS, notify_user=notify_user) return True @dbus_service.command def next_list_item( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next list item.""" tokens = [ "STRUCTURAL NAVIGATOR: next_list_item. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_list_items(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_LIST_ITEMS, notify_user=notify_user) return True @dbus_service.command def list_list_items( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of list items.""" tokens = [ "STRUCTURAL NAVIGATOR: list_list_items. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_list_items(script), guilabels.SN_TITLE_LIST_ITEM, [guilabels.SN_HEADER_LIST_ITEM], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # Live Regions # # # ######################## def _get_all_live_regions(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_live_regions(root, pred=pred) @dbus_service.command def previous_live_region( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous live region.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_live_region. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_live_regions(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_LIVE_REGIONS, notify_user=notify_user) return True @dbus_service.command def next_live_region( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next live region.""" tokens = [ "STRUCTURAL NAVIGATOR: next_live_region. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_live_regions(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_LIVE_REGIONS, notify_user=notify_user) return True def _last_live_region( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the last live region.""" tokens = [ "STRUCTURAL NAVIGATOR: last_live_region. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event live_region_presenter.get_presenter().go_last_live_region(script, event) return True ######################## # # # Paragraphs # # # ######################## def _get_all_paragraphs(self, script: default.Script) -> list[Atspi.Accessible]: def has_at_least_three_characters(obj): if AXUtilities.is_heading(obj): return True # We're choosing 3 characters as the minimum because some paragraphs contain a single # image or link and a text of length 2: An embedded object character and a space. # We want to skip these. return AXText.get_character_count(obj) > 2 def pred(x): if self.get_mode(script) == NavigationMode.GUI: return self._is_non_document_object(x) return has_at_least_three_characters(x) root = self._determine_root_container(script) return AXUtilities.find_all_paragraphs(root, True, pred=pred) @dbus_service.command def previous_paragraph( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous paragraph.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_paragraph. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_paragraphs(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_PARAGRAPHS, notify_user=notify_user) return True @dbus_service.command def next_paragraph( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next paragraph.""" tokens = [ "STRUCTURAL NAVIGATOR: next_paragraph. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_paragraphs(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_PARAGRAPHS, notify_user=notify_user) return True @dbus_service.command def list_paragraphs( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of paragraphs.""" tokens = [ "STRUCTURAL NAVIGATOR: list_paragraphs. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_paragraphs(script), guilabels.SN_TITLE_PARAGRAPH, [guilabels.SN_HEADER_PARAGRAPH], lambda obj: [self._get_item_string(script, obj)], notify_user=notify_user, ) return True ######################## # # # Radio Buttons # # # ######################## def _get_all_radio_buttons(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_radio_buttons(root, pred=pred) @dbus_service.command def previous_radio_button( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous radio button.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_radio_button. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_radio_buttons(script) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_RADIO_BUTTONS, notify_user=notify_user, ) return True @dbus_service.command def next_radio_button( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next radio button.""" tokens = [ "STRUCTURAL NAVIGATOR: next_radio_button. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_radio_buttons(script) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_RADIO_BUTTONS, notify_user=notify_user, ) return True @dbus_service.command def list_radio_buttons( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of radio buttons.""" tokens = [ "STRUCTURAL NAVIGATOR: list_radio_buttons. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_radio_buttons(script), guilabels.SN_TITLE_RADIO_BUTTON, [guilabels.SN_HEADER_RADIO_BUTTON, guilabels.SN_HEADER_STATE], lambda obj: [self._get_item_string(script, obj), self._get_state_string(obj)], notify_user=notify_user, ) return True ######################## # # # Separators # # # ######################## def _get_all_separators(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_separators(root, pred=pred) @dbus_service.command def previous_separator( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous separator.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_separator. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_separators(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_SEPARATORS, notify_user=notify_user) return True @dbus_service.command def next_separator( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next separator.""" tokens = [ "STRUCTURAL NAVIGATOR: next_separator. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_separators(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_SEPARATORS, notify_user=notify_user) return True ######################## # # # Tables # # # ######################## def _get_all_tables(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_tables(root, pred=pred) def _get_first_table_cell(self, table: Atspi.Accessible) -> Atspi.Accessible | None: # The reason we present the cell rather than the full table are twofold: # 1. Given a huge table, navigating to the cell and presenting the ancestor table is more # performant. # 2. When we calculate what's on the same line, it should be based on the cell's bounding # box; not the table's. # TODO - JD: Handle the second issue in the utilities which calculate the line. if not AXUtilities.is_table(table): return None if cell := AXTable.get_cell_at(table, 0, 0): return cell tokens = ["STRUCTURAL NAVIGATOR: Broken table interface for", table] debug.print_tokens(debug.LEVEL_INFO, tokens, True) cell = AXUtilities.get_table_cell(table) if cell: tokens = ["STRUCTURAL NAVIGATOR: Located", cell, "for first cell"] debug.print_tokens(debug.LEVEL_INFO, tokens, True) return None @dbus_service.command def previous_table( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous table.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_table. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_tables(script) result = self._get_object_in_direction(script, matches, False) obj = self._get_first_table_cell(result) or result self._present_object(script, obj, messages.NO_MORE_TABLES, notify_user=notify_user) return True @dbus_service.command def next_table( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next table.""" tokens = [ "STRUCTURAL NAVIGATOR: next_table. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_tables(script) result = self._get_object_in_direction(script, matches, True) obj = self._get_first_table_cell(result) or result self._present_object(script, obj, messages.NO_MORE_TABLES, notify_user=notify_user) return True @dbus_service.command def list_tables( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of tables.""" tokens = [ "STRUCTURAL NAVIGATOR: list_tables. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_tables(script), guilabels.SN_TITLE_TABLE, [guilabels.SN_HEADER_CAPTION, guilabels.SN_HEADER_DESCRIPTION], lambda obj: [ self._get_item_string(script, obj), AXUtilities.get_table_description_for_presentation(obj), ], notify_user=notify_user, ) return True ######################## # # # Unvisited Links # # # ######################## def _get_all_unvisited_links(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_unvisited_links(root, pred=pred) @dbus_service.command def previous_unvisited_link( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous unvisited link.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_unvisited_link. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_unvisited_links(script) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_UNVISITED_LINKS, notify_user=notify_user, ) return True @dbus_service.command def next_unvisited_link( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next unvisited link.""" tokens = [ "STRUCTURAL NAVIGATOR: next_unvisited_link. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_unvisited_links(script) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_UNVISITED_LINKS, notify_user=notify_user, ) return True @dbus_service.command def list_unvisited_links( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of unvisited links.""" tokens = [ "STRUCTURAL NAVIGATOR: list_unvisited_links. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_unvisited_links(script), guilabels.SN_TITLE_UNVISITED_LINK, [guilabels.SN_HEADER_LINK, guilabels.SN_HEADER_URI], lambda obj: [self._get_item_string(script, obj), AXHypertext.get_link_uri(obj)], notify_user=notify_user, ) return True ######################## # # # Visited Links # # # ######################## def _get_all_visited_links(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_visited_links(root, pred=pred) @dbus_service.command def previous_visited_link( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous visited link.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_visited_link. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_visited_links(script) result = self._get_object_in_direction(script, matches, False) self._present_object( script, result, messages.NO_MORE_VISITED_LINKS, notify_user=notify_user, ) return True @dbus_service.command def next_visited_link( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next visited link.""" tokens = [ "STRUCTURAL NAVIGATOR: next_visited_link. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_visited_links(script) result = self._get_object_in_direction(script, matches, True) self._present_object( script, result, messages.NO_MORE_VISITED_LINKS, notify_user=notify_user, ) return True @dbus_service.command def list_visited_links( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of visited links.""" tokens = [ "STRUCTURAL NAVIGATOR: list_visited_links. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_visited_links(script), guilabels.SN_TITLE_VISITED_LINK, [guilabels.SN_HEADER_LINK, guilabels.SN_HEADER_URI], lambda obj: [self._get_item_string(script, obj), AXHypertext.get_link_uri(obj)], notify_user=notify_user, ) return True ######################## # # # Links # # # ######################## def _get_all_links(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) return AXUtilities.find_all_links(root, pred=pred) @dbus_service.command def previous_link( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous link.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_link. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_links(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_LINKS, notify_user=notify_user) return True @dbus_service.command def next_link( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next link.""" tokens = [ "STRUCTURAL NAVIGATOR: next_link. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_links(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_LINKS, notify_user=notify_user) return True @dbus_service.command def list_links( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of links.""" tokens = [ "STRUCTURAL NAVIGATOR: list_links. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_links(script), guilabels.SN_TITLE_LINK, [guilabels.SN_HEADER_LINK, guilabels.SN_HEADER_STATE, guilabels.SN_HEADER_URI], lambda obj: [ self._get_item_string(script, obj), self._get_state_string(obj), AXHypertext.get_link_uri(obj), ], notify_user=notify_user, ) return True ######################## # # # Clickables # # # ######################## def _get_all_clickables(self, script: default.Script) -> list[Atspi.Accessible]: pred = None if self.get_mode(script) == NavigationMode.GUI: pred = self._is_non_document_object root = self._determine_root_container(script) result = AXUtilities.find_all_clickables(root, pred=pred) result += AXUtilities.find_all_focusable_objects_with_click_ancestor(root, pred=pred) return result @dbus_service.command def previous_clickable( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the previous clickable.""" tokens = [ "STRUCTURAL NAVIGATOR: previous_clickable. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_clickables(script) result = self._get_object_in_direction(script, matches, False) self._present_object(script, result, messages.NO_MORE_CLICKABLES, notify_user=notify_user) return True @dbus_service.command def next_clickable( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Goes to the next clickable.""" tokens = [ "STRUCTURAL NAVIGATOR: next_clickable. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event matches = self._get_all_clickables(script) result = self._get_object_in_direction(script, matches, True) self._present_object(script, result, messages.NO_MORE_CLICKABLES, notify_user=notify_user) return True @dbus_service.command def list_clickables( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays a list of clickables.""" tokens = [ "STRUCTURAL NAVIGATOR: list_clickables. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event self._present_object_list( script, self._get_all_clickables(script), guilabels.SN_TITLE_CLICKABLE, [guilabels.SN_HEADER_CLICKABLE, guilabels.SN_HEADER_ROLE], lambda obj: [ self._get_item_string(script, obj), AXUtilities.get_localized_role_name(obj), ], ) return True ######################## # # # Containers # # # ######################## def _get_current_container(self, script: default.Script) -> Atspi.Accessible | None: focus = focus_manager.get_manager().get_locus_of_focus() if container := AXUtilities.find_ancestor_inclusive(focus, AXUtilities.is_large_container): root = self._determine_root_container(script) if not AXUtilities.is_ancestor(container, root): return None return container @dbus_service.command def container_start( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the start of the current container.""" tokens = [ "STRUCTURAL NAVIGATOR: container_start. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event container = self._get_current_container(script) if container is None: if notify_user: presentation_manager.get_manager().present_message(messages.CONTAINER_NOT_IN_A) return True obj, offset = script.utilities.next_context(container, -1) self._present_line(script, obj, offset, notify_user) return True @dbus_service.command def container_end( self, script: default.Script, event: InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the end of the current container.""" tokens = [ "STRUCTURAL NAVIGATOR: container_end. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._last_input_event = event container = self._get_current_container(script) if container is None: if notify_user: presentation_manager.get_manager().present_message(messages.CONTAINER_NOT_IN_A) return True # Unlike going to the start of the container, when we move to the next edge # we pass beyond it on purpose. This makes us consistent with NVDA. obj, offset = script.utilities.last_context(container) next_object, next_offset = script.utilities.next_context(obj, offset) if next_object is None: next_object, next_offset = obj, offset self._present_line(script, next_object, next_offset, notify_user) return True _navigator = StructuralNavigator() def get_navigator() -> StructuralNavigator: """Returns the Structural Navigator""" return _navigator