# Orca # # Copyright 2005-2008 Sun Microsystems Inc. # Copyright 2016-2023 Igalia, S.L. # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the # Free Software Foundation, Inc., Franklin Street, Fifth Floor, # Boston MA 02110-1301 USA. # pylint: disable=too-many-lines # pylint: disable=too-many-public-methods """Module for flat-review commands""" from __future__ import annotations import time from typing import TYPE_CHECKING import gi gi.require_version("Gtk", "3.0") from gi.repository import Gtk from . import ( braille, braille_presenter, clipboard, cmdnames, command_manager, dbus_service, debug, flat_review, focus_manager, gsettings_registry, guilabels, input_event, keybindings, messages, presentation_manager, script_manager, speech_presenter, ) from .ax_event_synthesizer import AXEventSynthesizer from .ax_object import AXObject from .ax_text import AXText if TYPE_CHECKING: gi.require_version("Atspi", "2.0") from gi.repository import Atspi from .scripts import default @gsettings_registry.get_registry().gsettings_schema("org.gnome.Orca.FlatReview", name="flat-review") class FlatReviewPresenter: """Provides access to on-screen objects via flat-review.""" _SCHEMA = "flat-review" KEY_RESTRICTED = "restricted" def _get_setting(self, key: str, default: bool) -> bool: """Returns the dconf value for key, or default if not in dconf.""" return gsettings_registry.get_registry().layered_lookup( self._SCHEMA, key, "b", default=default, ) def __init__(self) -> None: self._context: flat_review.Context | None = None self._current_contents: str = "" self._restrict: bool = self.get_is_restricted() self._gui: FlatReviewContextGUI | None = None self._initialized: bool = False msg = "FLAT REVIEW PRESENTER: Registering D-Bus commands." debug.print_message(debug.LEVEL_INFO, msg, True) controller = dbus_service.get_remote_controller() controller.register_decorated_module("FlatReviewPresenter", 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_FLAT_REVIEW # Build keybinding mapping: cmd_name -> (desktop_kb, laptop_kb) def kb(keysym: str, mod: int, clicks: int = 1) -> keybindings.KeyBinding: return keybindings.KeyBinding(keysym, mod, click_count=clicks) cmd_bindings: dict[ str, tuple[keybindings.KeyBinding | None, keybindings.KeyBinding | None], ] = { "toggleFlatReviewModeHandler": ( kb("KP_Subtract", keybindings.NO_MODIFIER_MASK), kb("p", keybindings.ORCA_MODIFIER_MASK), ), "flatReviewSayAllHandler": ( kb("KP_Add", keybindings.NO_MODIFIER_MASK, 2), kb("semicolon", keybindings.ORCA_MODIFIER_MASK, 2), ), "reviewHomeHandler": ( kb("KP_Home", keybindings.ORCA_MODIFIER_MASK), kb("u", keybindings.ORCA_CTRL_MODIFIER_MASK), ), "reviewPreviousLineHandler": ( kb("KP_Home", keybindings.NO_MODIFIER_MASK), kb("u", keybindings.ORCA_MODIFIER_MASK), ), "reviewCurrentLineHandler": ( kb("KP_Up", keybindings.NO_MODIFIER_MASK), kb("i", keybindings.ORCA_MODIFIER_MASK), ), "reviewSpellCurrentLineHandler": ( kb("KP_Up", keybindings.NO_MODIFIER_MASK, 2), kb("i", keybindings.ORCA_MODIFIER_MASK, 2), ), "reviewPhoneticCurrentLineHandler": ( kb("KP_Up", keybindings.NO_MODIFIER_MASK, 3), kb("i", keybindings.ORCA_MODIFIER_MASK, 3), ), "reviewNextLineHandler": ( kb("KP_Page_Up", keybindings.NO_MODIFIER_MASK), kb("o", keybindings.ORCA_MODIFIER_MASK), ), "reviewEndHandler": ( kb("KP_Page_Up", keybindings.ORCA_MODIFIER_MASK), kb("o", keybindings.ORCA_CTRL_MODIFIER_MASK), ), "reviewPreviousItemHandler": ( kb("KP_Left", keybindings.NO_MODIFIER_MASK), kb("j", keybindings.ORCA_MODIFIER_MASK), ), "reviewAboveHandler": ( kb("KP_Left", keybindings.ORCA_MODIFIER_MASK), kb("j", keybindings.ORCA_CTRL_MODIFIER_MASK), ), "reviewCurrentItemHandler": ( kb("KP_Begin", keybindings.NO_MODIFIER_MASK), kb("k", keybindings.ORCA_MODIFIER_MASK), ), "reviewSpellCurrentItemHandler": ( kb("KP_Begin", keybindings.NO_MODIFIER_MASK, 2), kb("k", keybindings.ORCA_MODIFIER_MASK, 2), ), "reviewPhoneticCurrentItemHandler": ( kb("KP_Begin", keybindings.NO_MODIFIER_MASK, 3), kb("k", keybindings.ORCA_MODIFIER_MASK, 3), ), "reviewCurrentAccessibleHandler": ( kb("KP_Begin", keybindings.ORCA_MODIFIER_MASK), kb("k", keybindings.ORCA_CTRL_MODIFIER_MASK), ), "reviewNextItemHandler": ( kb("KP_Right", keybindings.NO_MODIFIER_MASK), kb("l", keybindings.ORCA_MODIFIER_MASK), ), "reviewBelowHandler": ( kb("KP_Right", keybindings.ORCA_MODIFIER_MASK), kb("l", keybindings.ORCA_CTRL_MODIFIER_MASK), ), "reviewPreviousCharacterHandler": ( kb("KP_End", keybindings.NO_MODIFIER_MASK), kb("m", keybindings.ORCA_MODIFIER_MASK), ), "reviewEndOfLineHandler": ( kb("KP_End", keybindings.ORCA_MODIFIER_MASK), kb("m", keybindings.ORCA_CTRL_MODIFIER_MASK), ), "reviewCurrentCharacterHandler": ( kb("KP_Down", keybindings.NO_MODIFIER_MASK), kb("comma", keybindings.ORCA_MODIFIER_MASK), ), "reviewSpellCurrentCharacterHandler": ( kb("KP_Down", keybindings.NO_MODIFIER_MASK, 2), kb("comma", keybindings.ORCA_MODIFIER_MASK, 2), ), "reviewUnicodeCurrentCharacterHandler": ( kb("KP_Down", keybindings.NO_MODIFIER_MASK, 3), kb("comma", keybindings.ORCA_MODIFIER_MASK, 3), ), "reviewNextCharacterHandler": ( kb("KP_Page_Down", keybindings.NO_MODIFIER_MASK), kb("period", keybindings.ORCA_MODIFIER_MASK), ), # Commands with no keybinding "reviewBottomLeftHandler": (None, None), "showContentsHandler": (None, None), "flatReviewCopyHandler": (None, None), "flatReviewAppendHandler": (None, None), "flatReviewToggleRestrictHandler": (None, None), } commands_data = [ ( "toggleFlatReviewModeHandler", self.toggle_flat_review_mode, cmdnames.TOGGLE_FLAT_REVIEW, ), ("reviewHomeHandler", self.go_home, cmdnames.REVIEW_HOME), ("reviewEndHandler", self.go_end, cmdnames.REVIEW_END), ("reviewBottomLeftHandler", self.go_bottom_left, cmdnames.REVIEW_BOTTOM_LEFT), ("reviewPreviousLineHandler", self.go_previous_line, cmdnames.REVIEW_PREVIOUS_LINE), ("reviewCurrentLineHandler", self.present_line, cmdnames.REVIEW_CURRENT_LINE), ("reviewNextLineHandler", self.go_next_line, cmdnames.REVIEW_NEXT_LINE), ("reviewSpellCurrentLineHandler", self.spell_line, cmdnames.REVIEW_SPELL_CURRENT_LINE), ( "reviewPhoneticCurrentLineHandler", self.phonetic_line, cmdnames.REVIEW_PHONETIC_CURRENT_LINE, ), ("reviewEndOfLineHandler", self.go_end_of_line, cmdnames.REVIEW_END_OF_LINE), ("reviewPreviousItemHandler", self.go_previous_item, cmdnames.REVIEW_PREVIOUS_ITEM), ("reviewCurrentItemHandler", self.present_item, cmdnames.REVIEW_CURRENT_ITEM), ("reviewNextItemHandler", self.go_next_item, cmdnames.REVIEW_NEXT_ITEM), ("reviewSpellCurrentItemHandler", self.spell_item, cmdnames.REVIEW_SPELL_CURRENT_ITEM), ( "reviewPhoneticCurrentItemHandler", self.phonetic_item, cmdnames.REVIEW_PHONETIC_CURRENT_ITEM, ), ( "reviewPreviousCharacterHandler", self.go_previous_character, cmdnames.REVIEW_PREVIOUS_CHARACTER, ), ( "reviewCurrentCharacterHandler", self.present_character, cmdnames.REVIEW_CURRENT_CHARACTER, ), ( "reviewSpellCurrentCharacterHandler", self.spell_character, cmdnames.REVIEW_SPELL_CURRENT_CHARACTER, ), ( "reviewUnicodeCurrentCharacterHandler", self.unicode_current_character, cmdnames.REVIEW_UNICODE_CURRENT_CHARACTER, ), ("reviewNextCharacterHandler", self.go_next_character, cmdnames.REVIEW_NEXT_CHARACTER), ( "reviewCurrentAccessibleHandler", self.present_object, cmdnames.REVIEW_CURRENT_ACCESSIBLE, ), ("reviewAboveHandler", self.go_above, cmdnames.REVIEW_ABOVE), ("reviewBelowHandler", self.go_below, cmdnames.REVIEW_BELOW), ("showContentsHandler", self.show_contents, cmdnames.FLAT_REVIEW_SHOW_CONTENTS), ("flatReviewCopyHandler", self.copy_to_clipboard, cmdnames.FLAT_REVIEW_COPY), ("flatReviewAppendHandler", self.append_to_clipboard, cmdnames.FLAT_REVIEW_APPEND), ("flatReviewSayAllHandler", self.say_all, cmdnames.SAY_ALL_FLAT_REVIEW), ( "flatReviewToggleRestrictHandler", self.toggle_restrict, cmdnames.TOGGLE_RESTRICT_FLAT_REVIEW, ), ] braille_bindings: dict[str, tuple[int, ...]] = {} bindings = [ ("reviewAboveHandler", braille.BRLAPI_KEY_CMD_LNUP), ("reviewBelowHandler", braille.BRLAPI_KEY_CMD_LNDN), ("toggleFlatReviewModeHandler", braille.BRLAPI_KEY_CMD_FREEZE), ("reviewHomeHandler", braille.BRLAPI_KEY_CMD_TOP_LEFT), ("reviewBottomLeftHandler", braille.BRLAPI_KEY_CMD_BOT_LEFT), ] for name, key in bindings: if key is not None: braille_bindings[name] = (key,) if not braille_bindings: msg = "FLAT REVIEW PRESENTER: Braille bindings unavailable." debug.print_message(debug.LEVEL_INFO, msg, True) for name, function, description in commands_data: desktop_kb, laptop_kb = cmd_bindings.get(name, (None, None)) manager.add_command( command_manager.KeyboardCommand( name, function, group_label, description, desktop_keybinding=desktop_kb, laptop_keybinding=laptop_kb, ), ) if name in braille_bindings: bb = braille_bindings[name] manager.add_command( command_manager.BrailleCommand( name, function, group_label, description, braille_bindings=bb, ), ) msg = "FLAT REVIEW PRESENTER: Commands set up." debug.print_message(debug.LEVEL_INFO, msg, True) def is_active(self) -> bool: """Returns True if the flat review presenter is active.""" return self._context is not None def get_or_create_context(self, script: default.Script | None = None) -> flat_review.Context: """Returns the flat review context, creating one if necessary.""" if not self._context: msg = f"FLAT REVIEW PRESENTER: Creating new context. Restrict: {self._restrict}" debug.print_message(debug.LEVEL_INFO, msg, True) if self._restrict: mode, obj = focus_manager.get_manager().get_active_mode_and_object_of_interest() self._context = flat_review.Context(script, root=obj) else: self._context = flat_review.Context(script) focus_manager.get_manager().emit_region_changed( self._context.get_current_object(), mode=focus_manager.FLAT_REVIEW, ) return self._context msg = f"FLAT REVIEW PRESENTER: Using existing context. Restrict: {self._restrict}" debug.print_message(debug.LEVEL_INFO, msg, True) # If we are in unrestricted mode, update the context as below. # If the context already exists, but the active mode is not flat review, update # the flat review location to that of the object of interest -- if the object of # interest is in the flat review context (which means it's on screen). In some # cases the object of interest will not be in the flat review context because it # is represented by descendant text objects. set_current_to_zone_with_object checks # for this condition and if it can find a zone whose ancestor is the object of # interest, it will set the current zone to the descendant, causing Orca to # present the text at the location of the object of interest. mode, obj = focus_manager.get_manager().get_active_mode_and_object_of_interest() obj = obj or focus_manager.get_manager().get_locus_of_focus() if ( mode != focus_manager.FLAT_REVIEW and obj != self._context.get_current_object() and not self._restrict ): tokens = [ "FLAT REVIEW PRESENTER: Attempting to update location from", self._context.get_current_object(), "to", obj, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context.set_current_to_zone_with_object(obj) # If we are restricting, and the current mode is not flat review, calculate a new context if self._restrict and mode != focus_manager.FLAT_REVIEW: msg = "FLAT REVIEW PRESENTER: Creating new restricted context." debug.print_message(debug.LEVEL_INFO, msg, True) self._context = flat_review.Context(script, obj) return self._context def start( self, script: default.Script | None = None, event: input_event.InputEvent | None = None, ) -> None: """Starts flat review.""" if self._context: msg = "FLAT REVIEW PRESENTER: Already in flat review" debug.print_message(debug.LEVEL_INFO, msg, True) return msg = "FLAT REVIEW PRESENTER: Starting flat review" debug.print_message(debug.LEVEL_INFO, msg, True) if script is None: script = script_manager.get_manager().get_active_script() assert script is not None self.get_or_create_context(script) if event is None: return if speech_presenter.get_presenter().use_verbose_speech(): presentation_manager.get_manager().present_message(messages.FLAT_REVIEW_START) self._item_presentation(script, event) def quit( self, script: default.Script | None = None, event: input_event.InputEvent | None = None, ) -> None: """Quits flat review.""" if self._context is None: msg = "FLAT REVIEW PRESENTER: Not in flat review" debug.print_message(debug.LEVEL_INFO, msg, True) return msg = "FLAT REVIEW PRESENTER: Quitting flat review" debug.print_message(debug.LEVEL_INFO, msg, True) self._context = None focus = focus_manager.get_manager().get_locus_of_focus() focus_manager.get_manager().emit_region_changed(focus, mode=focus_manager.FOCUS_TRACKING) if event is None or script is None: return if speech_presenter.get_presenter().use_verbose_speech(): presentation_manager.get_manager().present_message(messages.FLAT_REVIEW_STOP) script.update_braille(focus) @dbus_service.command def toggle_flat_review_mode( self, script: default.Script | None = None, event: input_event.InputEvent | None = None, notify_user: bool = False, ) -> bool: """Toggles between flat review mode and focus tracking mode.""" tokens = [ "FLAT REVIEW PRESENTER: toggle_flat_review_mode. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if self.is_active(): self.quit(script, event) return True self.start(script, event) return True @dbus_service.command def go_home( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the top left of the current window.""" tokens = [ "FLAT REVIEW PRESENTER: go_home. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) self._context.go_to_start_of(flat_review.Context.WINDOW) self.present_line(script, event) return True @dbus_service.command def go_end( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the bottom right of the current window.""" tokens = [ "FLAT REVIEW PRESENTER: go_end. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) self._context.go_to_end_of(flat_review.Context.WINDOW) self.present_line(script, event) return True @dbus_service.command def go_bottom_left( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the bottom left of the current window.""" tokens = [ "FLAT REVIEW PRESENTER: go_bottom_left. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) self._context.go_to_end_of(flat_review.Context.WINDOW) self._context.go_to_start_of(flat_review.Context.LINE) self.present_line(script, event) return True @dbus_service.command def go_previous_line( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the previous line.""" tokens = [ "FLAT REVIEW PRESENTER: go_previous_line. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if self._context.go_previous_line(): self.present_line(script, event) return True @dbus_service.command def present_line( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current line.""" tokens = [ "FLAT REVIEW PRESENTER: present_line. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._line_presentation(script, event, 1) return True @dbus_service.command def go_next_line( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the next line.""" tokens = [ "FLAT REVIEW PRESENTER: go_next_line. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if self._context.go_next_line(): self.present_line(script, event) return True @dbus_service.command def spell_line( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current line letter by letter.""" tokens = [ "FLAT REVIEW PRESENTER: spell_line. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._line_presentation(script, event, 2) return True @dbus_service.command def phonetic_line( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current line letter by letter phonetically.""" tokens = [ "FLAT REVIEW PRESENTER: phonetic_line. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._line_presentation(script, event, 3) return True @dbus_service.command def go_start_of_line( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the beginning of the current line.""" tokens = [ "FLAT REVIEW PRESENTER: go_start_of_line. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) self._context.go_to_start_of(flat_review.Context.LINE) self.present_character(script, event) return True @dbus_service.command def go_end_of_line( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the end of the line.""" tokens = [ "FLAT REVIEW PRESENTER: go_end_of_line. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) self._context.go_to_end_of(flat_review.Context.LINE) self.present_character(script, event) return True @dbus_service.command def go_previous_item( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the previous item or word.""" tokens = [ "FLAT REVIEW PRESENTER: go_previous_item. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if self._context.go_previous_word(): self.present_item(script, event) return True @dbus_service.command def present_item( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current item/word.""" tokens = [ "FLAT REVIEW PRESENTER: present_item. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._item_presentation(script, event, 1) return True @dbus_service.command def go_next_item( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the next item or word.""" tokens = [ "FLAT REVIEW PRESENTER: go_next_item. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if self._context.go_next_word(): self.present_item(script, event) return True @dbus_service.command def spell_item( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current item/word letter by letter.""" tokens = [ "FLAT REVIEW PRESENTER: spell_item. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._item_presentation(script, event, 2) return True @dbus_service.command def phonetic_item( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current word letter by letter phonetically.""" tokens = [ "FLAT REVIEW PRESENTER: phonetic_item. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._item_presentation(script, event, 3) return True @dbus_service.command def go_previous_character( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the previous character.""" tokens = [ "FLAT REVIEW PRESENTER: go_previous_character. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if self._context.go_previous_character(): self.present_character(script, event) return True @dbus_service.command def present_character( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current character.""" tokens = [ "FLAT REVIEW PRESENTER: present_character. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._character_presentation(script, event, 1) return True @dbus_service.command def go_next_character( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the next character.""" tokens = [ "FLAT REVIEW PRESENTER: go_next_character. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if self._context.go_next_character(): self.present_character(script, event) return True @dbus_service.command def spell_character( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current character phonetically.""" tokens = [ "FLAT REVIEW PRESENTER: spell_character. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._character_presentation(script, event, 2) return True @dbus_service.command def unicode_current_character( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current character's unicode value.""" tokens = [ "FLAT REVIEW PRESENTER: unicode_current_character. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._character_presentation(script, event, 3) return True @dbus_service.command def go_above( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the character above.""" tokens = [ "FLAT REVIEW PRESENTER: go_above. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if self._context.go_up(): self.present_item(script, event) return True @dbus_service.command def go_below( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Moves to the character below.""" tokens = [ "FLAT REVIEW PRESENTER: go_below. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if self._context.go_down(): self.present_item(script, event) return True @dbus_service.command def get_current_object( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> Atspi.Accessible: """Returns the current accessible object.""" tokens = [ "FLAT REVIEW PRESENTER: get_current_object. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) return self._context.get_current_object() @dbus_service.command def present_object( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Presents the current accessible object.""" tokens = [ "FLAT REVIEW PRESENTER: present_object. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) if not isinstance(event, input_event.BrailleEvent): script.present_object(self._context.get_current_object(), speechonly=True) focus_manager.get_manager().emit_region_changed( self._context.get_current_object(), mode=focus_manager.FLAT_REVIEW, ) return True @dbus_service.command def left_click_on_object( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Attempts to synthesize a left click on the current accessible.""" tokens = [ "FLAT REVIEW PRESENTER: left_click_on_object. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) obj = self._context.get_current_object() offset = self._context.get_current_text_offset() if offset >= 0 and AXEventSynthesizer.click_character(obj, offset, 1): return True return AXEventSynthesizer.click_object(obj, 1) @dbus_service.command def right_click_on_object( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Attempts to synthesize a right click on the current accessible.""" tokens = [ "FLAT REVIEW PRESENTER: right_click_on_object. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) obj = self._context.get_current_object() offset = self._context.get_current_text_offset() if offset >= 0 and AXEventSynthesizer.click_character(obj, offset, 3): return True return AXEventSynthesizer.click_object(obj, 3) @dbus_service.command def route_pointer_to_object( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Routes the mouse pointer to the current accessible.""" tokens = [ "FLAT REVIEW PRESENTER: route_pointer_to_object. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._context = self.get_or_create_context(script) obj = self._context.get_current_object() offset = self._context.get_current_text_offset() if offset >= 0 and AXEventSynthesizer.route_to_character(obj, offset): return True return AXEventSynthesizer.route_to_object(obj) def _update_braille(self, script: default.Script) -> None: """Obtains the braille regions for the current flat review line and displays them.""" if not braille_presenter.get_presenter().use_braille(): return self._context = self.get_or_create_context(script) regions, focused_region = self._context.get_current_braille_regions() if not regions: braille_presenter.get_presenter().refresh_braille() return braille_presenter.get_presenter().present_regions( list(regions), focused_region, ) def pan_braille_left( self, script: default.Script, _event: input_event.InputEvent | None = None, ) -> bool: """Pans the braille display left.""" self._context = self.get_or_create_context(script) # Try to pan left. If we couldn't (at edge), move to previous line. if not braille_presenter.get_presenter().pan_left(): self._context.go_to_start_of(flat_review.Context.LINE) self._context.go_previous_character() self._update_braille(script) return True # After panning, find a region that has a zone attribute to sync flat review context. # We do cell-by-cell panning here because not all braille regions have zones (e.g., # buttons don't have zones, only text regions do). This finds the first region with # a zone so we can update the flat review position accordingly. region_info = braille.get_region_at_cell(1) braille_region = region_info.region offset_in_zone = region_info.offset_in_region while braille_region and not hasattr(braille_region, "zone"): # Cell-by-cell panning (amount=1) to find the first region with a zone attribute. if not braille.pan_left(1): break region_info = braille.get_region_at_cell(1) braille_region = region_info.region offset_in_zone = region_info.offset_in_region if braille_region and not hasattr(braille_region, "zone"): self._context.go_to_start_of(flat_review.Context.LINE) self._context.go_previous_character() self._update_braille(script) elif braille_region and hasattr(braille_region, "zone"): self._context.set_current_zone(braille_region.zone, offset_in_zone) braille_presenter.get_presenter().refresh_braille(pan_to_cursor=False) return True def pan_braille_right( self, script: default.Script, _event: input_event.InputEvent | None = None, ) -> bool: """Pans the braille display right.""" self._context = self.get_or_create_context(script) # Try to pan right. If we couldn't (at edge), move to next line. if not braille_presenter.get_presenter().pan_right(): self._context.go_next_line() self._update_braille(script) return True # After panning, find a region that has a zone attribute to sync flat review context. # We do cell-by-cell panning here because not all braille regions have zones (e.g., # buttons don't have zones, only text regions do). This finds the first region with # a zone so we can update the flat review position accordingly. region_info = braille.get_region_at_cell(1) braille_region = region_info.region offset_in_zone = region_info.offset_in_region while braille_region and not hasattr(braille_region, "zone"): # Cell-by-cell panning (amount=1) to find the first region with a zone attribute. if not braille.pan_right(1): break region_info = braille.get_region_at_cell(1) braille_region = region_info.region offset_in_zone = region_info.offset_in_region if braille_region and not hasattr(braille_region, "zone"): self._context.go_next_line() self._update_braille(script) elif braille_region and hasattr(braille_region, "zone"): self._context.set_current_zone(braille_region.zone, offset_in_zone) braille_presenter.get_presenter().refresh_braille(pan_to_cursor=False) return True def _get_all_lines( self, script: default.Script, _event: input_event.InputEvent | None = None, ) -> tuple[list[tuple[str, Atspi.Accessible | None]], tuple[int, int, int, int]]: """Returns a (list of (line_string, obj) tuples, current location) tuple.""" lines: list[tuple[str, Atspi.Accessible | None]] = [] self._context = self.get_or_create_context(script) location = self._context.get_current_location() self._context.go_to_start_of(flat_review.Context.WINDOW) string = self._context.get_current_line_string() while string is not None: obj = self._context.get_current_object() lines.append((string.rstrip("\n"), obj)) if not self._context.go_next_line(): break string = self._context.get_current_line_string() return lines, location @dbus_service.command def say_all( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Speaks the contents of the entire window.""" tokens = [ "FLAT REVIEW PRESENTER: say_all. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) for string, obj in self._get_all_lines(script, event)[0]: if not string.isspace(): presentation_manager.get_manager().speak_accessible_text(obj, string) return True @dbus_service.command def show_contents( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Displays the entire flat review contents in a text view.""" tokens = [ "FLAT REVIEW PRESENTER: show_contents. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) line_tuples, location = self._get_all_lines(script, event) text = "\n".join(string for string, _obj in line_tuples) title = guilabels.FLAT_REVIEW_CONTENTS self._gui = FlatReviewContextGUI(script, title, text, location) self._gui.show_gui() return True @dbus_service.command def copy_to_clipboard( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Copies the string just presented to the clipboard.""" tokens = [ "FLAT REVIEW PRESENTER: copy_to_clipboard. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not self.is_active(): if notify_user: presentation_manager.get_manager().present_message(messages.FLAT_REVIEW_NOT_IN) return True clipboard.get_presenter().set_text(self._current_contents.rstrip("\n")) if notify_user: presentation_manager.get_manager().present_message(messages.FLAT_REVIEW_COPIED) return True @dbus_service.command def append_to_clipboard( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Appends the string just presented to the clipboard.""" tokens = [ "FLAT REVIEW PRESENTER: append_to_clipboard. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) if not self.is_active(): if notify_user: presentation_manager.get_manager().present_message(messages.FLAT_REVIEW_NOT_IN) return True clipboard.get_presenter().append_text(self._current_contents.rstrip("\n")) if notify_user: presentation_manager.get_manager().present_message(messages.FLAT_REVIEW_APPENDED) return True @gsettings_registry.get_registry().gsetting( key=KEY_RESTRICTED, schema="flat-review", gtype="b", default=False, summary="Restrict flat review to current object", migration_key="flatReviewIsRestricted", ) @dbus_service.getter def get_is_restricted(self) -> bool: """Returns whether flat review is restricted to the current object.""" return self._get_setting(self.KEY_RESTRICTED, False) @dbus_service.setter def set_is_restricted(self, value: bool) -> bool: """Sets whether flat review is restricted to the current object.""" msg = f"FLAT REVIEW PRESENTER: Setting is-restricted to {value}." debug.print_message(debug.LEVEL_INFO, msg, True) gsettings_registry.get_registry().set_runtime_value( self._SCHEMA, self.KEY_RESTRICTED, value ) return True @dbus_service.command def toggle_restrict( self, script: default.Script, event: input_event.InputEvent | None = None, notify_user: bool = True, ) -> bool: """Toggles the restricting of flat review to the current object.""" tokens = [ "FLAT REVIEW PRESENTER: toggle_restrict. Script:", script, "Event:", event, "notify_user:", notify_user, ] debug.print_tokens(debug.LEVEL_INFO, tokens, True) self._restrict = not self._restrict self.set_is_restricted(self._restrict) if self._restrict: if notify_user: presentation_manager.get_manager().present_message(messages.FLAT_REVIEW_RESTRICTED) elif notify_user: presentation_manager.get_manager().present_message(messages.FLAT_REVIEW_UNRESTRICTED) if self.is_active(): # Reset the context self._context = None self.start() return True def _line_presentation( self, script: default.Script, event: input_event.InputEvent | None = None, speech_type: int = 1, ) -> bool: """Presents the current line.""" self._context = self.get_or_create_context(script) line_string = self._context.get_current_line_string() if not isinstance(event, input_event.BrailleEvent): presenter = presentation_manager.get_manager() if not line_string or line_string == "\n": presenter.speak_message(messages.BLANK) elif line_string.isspace(): presenter.speak_message(messages.WHITE_SPACE) elif line_string.isupper() and (speech_type < 2 or speech_type > 3): presenter.speak_accessible_text(self._context.get_current_object(), line_string) elif speech_type == 2: presenter.spell_item(line_string) elif speech_type == 3: presenter.spell_phonetically(line_string) else: presenter.speak_accessible_text(self._context.get_current_object(), line_string) focus_manager.get_manager().emit_region_changed( self._context.get_current_object(), mode=focus_manager.FLAT_REVIEW, ) self._update_braille(script) self._current_contents = line_string return True def _item_presentation( self, script: default.Script, event: input_event.InputEvent | None = None, speech_type: int = 1, ) -> bool: """Presents the current item/word.""" self._context = self.get_or_create_context(script) word_string = self._context.get_current_word_string() if not isinstance(event, input_event.BrailleEvent): presenter = presentation_manager.get_manager() if not word_string or word_string == "\n": presenter.speak_message(messages.BLANK) else: line_string = self._context.get_current_line_string() if line_string == "\n": presenter.speak_message(messages.BLANK) elif word_string.isspace(): presenter.speak_message(messages.WHITE_SPACE) elif word_string.isupper() and speech_type == 1: presenter.speak_accessible_text(self._context.get_current_object(), word_string) elif speech_type == 2: presenter.spell_item(word_string) elif speech_type == 3: presenter.spell_phonetically(word_string) elif speech_type == 1: presenter.speak_accessible_text(self._context.get_current_object(), word_string) focus_manager.get_manager().emit_region_changed( self._context.get_current_object(), mode=focus_manager.FLAT_REVIEW, ) self._update_braille(script) self._current_contents = word_string return True def _character_presentation( self, script: default.Script, event: input_event.InputEvent | None = None, speech_type: int = 1, ) -> bool: """Presents the current character.""" focus = focus_manager.get_manager().get_locus_of_focus() if not self._context and AXObject.supports_text(focus): char_string = AXText.get_character_at_offset(focus)[0] else: self._context = self.get_or_create_context(script) char_string = self._context.get_current_character_string() if not isinstance(event, input_event.BrailleEvent): presenter = presentation_manager.get_manager() if not char_string or (char_string == "\n" and speech_type != 3): presenter.speak_message(messages.BLANK) elif speech_type == 3: presenter.speak_message(messages.UNICODE % f"{ord(char_string):04x}") elif speech_type == 2: presenter.spell_phonetically(char_string) else: presenter.speak_character(char_string) if not self._context: return True focus_manager.get_manager().emit_region_changed( self._context.get_current_object(), mode=focus_manager.FLAT_REVIEW, ) self._update_braille(script) self._current_contents = char_string return True class FlatReviewContextGUI: """Presents the entire flat review context in a text view""" def __init__( self, script: default.Script, title: str, text: str, location: tuple[int, int, int, int], ) -> None: self._script: default.Script = script self._gui: Gtk.Dialog = self._create_dialog(title, text, location) def _create_dialog( self, title: str, text: str, location: tuple[int, int, int, int], ) -> Gtk.Dialog: """Creates the dialog.""" dialog = Gtk.Dialog( title, None, Gtk.DialogFlags.MODAL, (Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE), ) dialog.set_default_size(800, 600) scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) scrolled_window.set_hexpand(True) scrolled_window.set_vexpand(True) textbuffer = Gtk.TextBuffer() textbuffer.set_text(text) line_index, _zone_index, word_index, char_index = location start_iter = textbuffer.get_start_iter() start_iter.forward_lines(line_index) for _ in range(word_index): start_iter.forward_word_end() # This is needed to get past any punctuation. Note that we cannot check starts_word # here. Example: The letter after an apostrophe is reported as a word start. while not start_iter.get_char().isspace(): if not start_iter.forward_char(): break # The text iter word can start with one or more spaces. Move to the beginning of the flat # review word before advancing by character. while not start_iter.starts_word(): if not start_iter.forward_char(): break start_iter.forward_chars(char_index) textbuffer.place_cursor(start_iter) textview = Gtk.TextView(buffer=textbuffer) textview.set_wrap_mode(Gtk.WrapMode.WORD) scrolled_window.add(textview) # pylint: disable=no-member dialog.get_content_area().pack_start(scrolled_window, True, True, 0) dialog.connect("response", self.on_response) return dialog def on_response(self, _dialog: Gtk.Dialog, response: Gtk.ResponseType) -> None: """Handler for the 'response' signal of the dialog.""" if response == Gtk.ResponseType.CLOSE: self._gui.destroy() def show_gui(self) -> None: """Shows the dialog.""" self._gui.show_all() # pylint: disable=no-member self._gui.present_with_time(time.time()) _presenter: FlatReviewPresenter = FlatReviewPresenter() def get_presenter() -> FlatReviewPresenter: """Returns the Flat Review Presenter""" return _presenter