# Orca
#
# Copyright 2004-2009 Sun Microsystems Inc.
# Copyright 2011-2025 Igalia, S.L.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

# pylint: disable=too-many-lines
# pylint: disable=too-many-arguments
# pylint: disable=too-many-locals
# pylint: disable=too-many-positional-arguments
# pylint: disable=too-many-public-methods

"""The default Script for presenting information to the user."""

from __future__ import annotations

import re
from typing import TYPE_CHECKING

from orca import (
    action_presenter,
    ax_event_synthesizer,
    braille,
    braille_presenter,
    bypass_mode_manager,
    caret_navigator,
    chat_presenter,
    clipboard,
    cmdnames,
    command_manager,
    debug,
    debugging_tools_manager,
    document_presenter,
    event_manager,
    flat_review_finder,
    flat_review_presenter,
    focus_manager,
    guilabels,
    input_event,
    input_event_manager,
    keybindings,
    learn_mode_presenter,
    live_region_presenter,
    messages,
    mouse_review,
    notification_presenter,
    object_navigator,
    orca,
    orca_gui_prefs,
    presentation_manager,
    profile_manager,
    say_all_presenter,
    script,
    script_manager,
    sleep_mode_manager,
    speech_manager,
    speech_presenter,
    spellcheck_presenter,
    structural_navigator,
    system_information_presenter,
    table_navigator,
    typing_echo_presenter,
    where_am_i_presenter,
)
from orca.ax_document import AXDocument
from orca.ax_object import AXObject
from orca.ax_selection import AXSelection
from orca.ax_text import AXText
from orca.ax_utilities import AXUtilities
from orca.ax_utilities_event import TextEventReason
from orca.ax_utilities_text import TextUnit

if TYPE_CHECKING:
    from collections.abc import Callable

    import gi

    gi.require_version("Atspi", "2.0")
    from gi.repository import Atspi


class Script(script.Script):
    """The default Script for presenting information to the user."""

    _commands_initialized: bool = False

    def _get_all_extensions(self) -> list[tuple[Callable, str]]:
        """Returns (extension_getter, localized_name) for each extension."""

        return [
            (braille_presenter.get_presenter, guilabels.BRAILLE),
            (notification_presenter.get_presenter, guilabels.KB_GROUP_NOTIFICATIONS),
            (clipboard.get_presenter, guilabels.KB_GROUP_CLIPBOARD),
            (command_manager.get_manager, guilabels.KB_GROUP_DEFAULT),
            (say_all_presenter.get_presenter, guilabels.KB_GROUP_DEFAULT),
            (typing_echo_presenter.get_presenter, guilabels.KB_GROUP_DEFAULT),
            (speech_manager.get_manager, guilabels.KB_GROUP_SPEECH_VERBOSITY),
            (speech_presenter.get_presenter, guilabels.KB_GROUP_SPEECH_VERBOSITY),
            (bypass_mode_manager.get_manager, guilabels.KB_GROUP_DEFAULT),
            (sleep_mode_manager.get_manager, guilabels.KB_GROUP_SLEEP_MODE),
            (system_information_presenter.get_presenter, guilabels.KB_GROUP_SYSTEM_INFORMATION),
            (object_navigator.get_navigator, guilabels.KB_GROUP_OBJECT_NAVIGATION),
            (caret_navigator.get_navigator, guilabels.KB_GROUP_CARET_NAVIGATION),
            (structural_navigator.get_navigator, guilabels.KB_GROUP_STRUCTURAL_NAVIGATION),
            (table_navigator.get_navigator, guilabels.KB_GROUP_TABLE_NAVIGATION),
            (document_presenter.get_presenter, guilabels.KB_GROUP_DOCUMENTS),
            (live_region_presenter.get_presenter, guilabels.KB_GROUP_LIVE_REGIONS),
            (learn_mode_presenter.get_presenter, guilabels.KB_GROUP_LEARN_MODE),
            (mouse_review.get_reviewer, guilabels.KB_GROUP_MOUSE_REVIEW),
            (action_presenter.get_presenter, guilabels.KB_GROUP_ACTIONS),
            (flat_review_presenter.get_presenter, guilabels.KB_GROUP_FLAT_REVIEW),
            (flat_review_finder.get_finder, guilabels.KB_GROUP_FIND),
            (where_am_i_presenter.get_presenter, guilabels.KB_GROUP_WHERE_AM_I),
            (debugging_tools_manager.get_manager, guilabels.KB_GROUP_DEBUGGING_TOOLS),
            (chat_presenter.get_presenter, guilabels.KB_GROUP_CHAT),
            (profile_manager.get_manager, guilabels.GENERAL_PROFILES),
        ]

    def set_up_commands(self) -> None:
        """Sets up commands with CommandManager."""

        tokens = ["DEFAULT: Setting up commands for", self.app]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if Script._commands_initialized:
            msg = "DEFAULT: Commands already initialized."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return

        Script._commands_initialized = True

        manager = command_manager.get_manager()
        group_label = guilabels.KB_GROUP_DEFAULT

        kb_kp_divide_orca = keybindings.KeyBinding("KP_Divide", keybindings.ORCA_MODIFIER_MASK)
        kb_kp_divide = keybindings.KeyBinding("KP_Divide", keybindings.NO_MODIFIER_MASK)
        kb_kp_multiply = keybindings.KeyBinding("KP_Multiply", keybindings.NO_MODIFIER_MASK)
        kb_9_orca = keybindings.KeyBinding("9", keybindings.ORCA_MODIFIER_MASK)
        kb_7_orca = keybindings.KeyBinding("7", keybindings.ORCA_MODIFIER_MASK)
        kb_8_orca = keybindings.KeyBinding("8", keybindings.ORCA_MODIFIER_MASK)
        kb_space_orca = keybindings.KeyBinding("space", keybindings.ORCA_MODIFIER_MASK)
        kb_space_ctrl_orca = keybindings.KeyBinding("space", keybindings.ORCA_CTRL_MODIFIER_MASK)

        script_commands: list[
            tuple[
                str,
                Callable[..., bool],
                str,
                keybindings.KeyBinding | None,
                keybindings.KeyBinding | None,
            ]
        ] = [
            (
                "routePointerToItemHandler",
                self.route_pointer_to_item,
                cmdnames.ROUTE_POINTER_TO_ITEM,
                kb_kp_divide_orca,
                kb_9_orca,
            ),
            (
                "leftClickReviewItemHandler",
                self.left_click_item,
                cmdnames.LEFT_CLICK_REVIEW_ITEM,
                kb_kp_divide,
                kb_7_orca,
            ),
            (
                "rightClickReviewItemHandler",
                self.right_click_item,
                cmdnames.RIGHT_CLICK_REVIEW_ITEM,
                kb_kp_multiply,
                kb_8_orca,
            ),
            (
                "panBrailleLeftHandler",
                self.pan_braille_left,
                cmdnames.PAN_BRAILLE_LEFT,
                None,
                None,
            ),
            (
                "panBrailleRightHandler",
                self.pan_braille_right,
                cmdnames.PAN_BRAILLE_RIGHT,
                None,
                None,
            ),
            (
                "contractedBrailleHandler",
                self.set_contracted_braille,
                cmdnames.SET_CONTRACTED_BRAILLE,
                None,
                None,
            ),
            ("shutdownHandler", self.quit_orca, cmdnames.QUIT_ORCA, None, None),
            (
                "preferencesSettingsHandler",
                self.show_preferences_gui,
                cmdnames.SHOW_PREFERENCES_GUI,
                kb_space_orca,
                kb_space_orca,
            ),
            (
                "appPreferencesSettingsHandler",
                self.show_app_preferences_gui,
                cmdnames.SHOW_APP_PREFERENCES_GUI,
                kb_space_ctrl_orca,
                kb_space_ctrl_orca,
            ),
        ]

        for (
            name,
            function,
            description,
            desktop_kb,
            laptop_kb,
        ) in script_commands:
            manager.add_command(
                command_manager.KeyboardCommand(
                    name,
                    function,
                    group_label,
                    description,
                    desktop_keybinding=desktop_kb,
                    laptop_keybinding=laptop_kb,
                ),
            )

        braille_bindings: dict[str, tuple[int, ...]] = {}
        left_keys = tuple(
            key
            for key in (
                braille.BRLAPI_KEY_CMD_HWINLT,
                braille.BRLAPI_KEY_CMD_FWINLT,
                braille.BRLAPI_KEY_CMD_FWINLTSKIP,
            )
            if key is not None
        )
        right_keys = tuple(
            key
            for key in (
                braille.BRLAPI_KEY_CMD_HWINRT,
                braille.BRLAPI_KEY_CMD_FWINRT,
                braille.BRLAPI_KEY_CMD_FWINRTSKIP,
            )
            if key is not None
        )
        if left_keys:
            braille_bindings["panBrailleLeftHandler"] = left_keys
        if right_keys:
            braille_bindings["panBrailleRightHandler"] = right_keys

        single_key_bindings: list[tuple[str, int | None]] = [
            ("goBrailleHomeHandler", braille.BRLAPI_KEY_CMD_HOME),
            ("contractedBrailleHandler", braille.BRLAPI_KEY_CMD_SIXDOTS),
            ("processRoutingKeyHandler", braille.BRLAPI_KEY_CMD_ROUTE),
            ("processBrailleCutBeginHandler", braille.BRLAPI_KEY_CMD_CUTBEGIN),
            ("processBrailleCutLineHandler", braille.BRLAPI_KEY_CMD_CUTLINE),
        ]
        for handler_name, key in single_key_bindings:
            if key is not None:
                braille_bindings[handler_name] = (key,)
        if not braille_bindings:
            msg = "DEFAULT: Braille bindings unavailable."
            debug.print_message(debug.LEVEL_INFO, msg, True)

        braille_commands: list[tuple[str, Callable[..., bool], str, bool]] = [
            ("panBrailleLeftHandler", self.pan_braille_left, cmdnames.PAN_BRAILLE_LEFT, True),
            ("panBrailleRightHandler", self.pan_braille_right, cmdnames.PAN_BRAILLE_RIGHT, True),
            (
                "contractedBrailleHandler",
                self.set_contracted_braille,
                cmdnames.SET_CONTRACTED_BRAILLE,
                True,
            ),
            ("goBrailleHomeHandler", self.go_braille_home, cmdnames.GO_BRAILLE_HOME, False),
            (
                "processRoutingKeyHandler",
                self.process_routing_key,
                cmdnames.PROCESS_ROUTING_KEY,
                False,
            ),
            (
                "processBrailleCutBeginHandler",
                self.process_braille_cut_begin,
                cmdnames.PROCESS_BRAILLE_CUT_BEGIN,
                False,
            ),
            (
                "processBrailleCutLineHandler",
                self.process_braille_cut_line,
                cmdnames.PROCESS_BRAILLE_CUT_LINE,
                False,
            ),
        ]

        for name, function, description, executes_in_learn_mode in braille_commands:
            bb = braille_bindings.get(name, ())
            manager.add_command(
                command_manager.BrailleCommand(
                    name,
                    function,
                    group_label,
                    description,
                    braille_bindings=bb,
                    executes_in_learn_mode=executes_in_learn_mode,
                ),
            )

        for extension_getter, _localized_name in self._get_all_extensions():
            extension_getter().set_up_commands()

        all_braille_keys: set[int] = set()
        for cmd in manager.get_all_braille_commands():
            all_braille_keys.update(cmd.get_braille_bindings())
        braille.setup_key_ranges(all_braille_keys)

        cmd_count = len(command_manager.get_manager().get_all_keyboard_commands())
        msg = f"DEFAULT: Commands set up: {cmd_count} keyboard commands"
        debug.print_message(debug.LEVEL_INFO, msg, True)

    def register_event_listeners(self) -> None:
        """Registers for listeners needed by this script."""

        event_manager.get_manager().register_script_listeners(self)

    def deregister_event_listeners(self) -> None:
        """De-registers the listeners needed by this script."""

        event_manager.get_manager().deregister_script_listeners(self)

    def _get_queued_event(
        self,
        event_type: str,
        detail1: int | None = None,
        detail2: int | None = None,
        any_data=None,
    ) -> Atspi.Event | None:
        cached_event = self.event_cache.get(event_type, [None, 0])[0]
        if not cached_event:
            tokens = ["SCRIPT: No queued event of type", event_type]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return None

        if detail1 is not None and detail1 != cached_event.detail1:
            tokens = [
                "SCRIPT: Queued event's detail1 (",
                str(cached_event.detail1),
                ") doesn't match",
                str(detail1),
            ]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return None

        if detail2 is not None and detail2 != cached_event.detail2:
            tokens = [
                "SCRIPT: Queued event's detail2 (",
                str(cached_event.detail2),
                ") doesn't match",
                str(detail2),
            ]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return None

        if any_data is not None and any_data != cached_event.any_data:
            tokens = [
                "SCRIPT: Queued event's any_data (",
                cached_event.any_data,
                ") doesn't match",
                any_data,
            ]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return None

        tokens = ["SCRIPT: Found matching queued event:", cached_event]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        return cached_event

    def locus_of_focus_changed(self, event, old_focus, new_focus):
        """Called when the visual object with focus changes."""

        presentation_manager.get_manager().present_command_announcement()

        if not new_focus:
            return True

        if AXUtilities.is_defunct(new_focus):
            return True

        is_name_change = event and event.type.endswith("accessible-name")
        if old_focus == new_focus and not is_name_change:
            msg = "DEFAULT: old focus == new focus"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.run_find_command_on:
            if self.run_find_command_on == new_focus:
                self.run_find_command_on = None
                flat_review_finder.get_finder().find(self)
            return True

        if flat_review_presenter.get_presenter().is_active():
            flat_review_presenter.get_presenter().quit()

        if learn_mode_presenter.get_presenter().is_active():
            learn_mode_presenter.get_presenter().quit()

        document_presenter.get_presenter().update_mode_if_needed(self, old_focus, new_focus)

        active_window = self.utilities.top_level_object(new_focus)
        focus_manager.get_manager().set_active_window(active_window)
        if old_focus is None:
            old_focus = active_window

        manager = presentation_manager.get_manager()
        manager.interrupt_if_needed_for_focus_change(old_focus, new_focus, event)
        manager.present_object(self, new_focus, priorObj=old_focus)
        return True

    def activate(self) -> None:
        """Called when this script is activated."""

        tokens = ["DEFAULT: Activating script for", self.app]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if not focus_manager.get_manager().is_in_preferences_window():
            speech_manager.get_manager().update_punctuation_level()
            speech_manager.get_manager().update_capitalization_style()
            speech_manager.get_manager().update_synthesizer()

        presenter = document_presenter.get_presenter()
        if presenter.has_state_for_app(self.app):
            presenter.restore_mode_for_script(self)
        else:
            reason = "script activation, no prior state"
            presenter.suspend_navigators(self, False, reason)
            structural_navigator.get_navigator().set_mode(self, self._default_sn_mode)
            caret_navigator.get_navigator().set_enabled_for_script(
                self,
                self._default_caret_navigation_enabled,
            )

        table_navigator.get_navigator().refresh_enabled_state()
        command_manager.get_manager().activate_commands(f"activated {self.name}")

    def deactivate(self) -> None:
        """Called when this script is deactivated."""

        if bypass_mode_manager.get_manager().is_active():
            bypass_mode_manager.get_manager().toggle_enabled(self)

    def update_braille(self, obj: Atspi.Accessible, **args) -> None:
        """Updates the braille display to show obj."""

        if not obj:
            return
        braille_presenter.get_presenter().present_generated_braille(self, obj, **args)

    ########################################################################
    #                                                                      #
    # INPUT EVENT HANDLERS (AKA ORCA COMMANDS)                             #
    #                                                                      #
    ########################################################################

    def show_app_preferences_gui(
        self,
        current_script: Script | None = None,
        _event: input_event.InputEvent | None = None,
    ) -> bool:
        """Shows the app Preferences dialog."""

        current_script = current_script or self
        ui = orca_gui_prefs.OrcaSetupGUI(current_script)
        ui.show_gui()
        return True

    def show_preferences_gui(
        self,
        _script: Script | None = None,
        _event: input_event.InputEvent | None = None,
    ) -> bool:
        """Displays the Preferences dialog."""

        default_script = script_manager.get_manager().get_default_script()
        script_manager.get_manager().set_active_script(default_script, "Global preferences")
        ui = orca_gui_prefs.OrcaSetupGUI(default_script)
        ui.show_gui()
        return True

    def quit_orca(
        self,
        _script: Script | None = None,
        _event: input_event.InputEvent | None = None,
    ) -> bool:
        """Quit Orca."""

        orca.shutdown()
        return True

    def pan_braille_left(
        self,
        current_script: Script | None = None,
        event: input_event.InputEvent | None = None,
    ) -> bool:
        """Pans the braille display to the left."""

        if (
            isinstance(event, input_event.KeyboardEvent)
            and not braille_presenter.get_presenter().use_braille()
        ):
            msg = "DEFAULT: panBrailleLeft command requires braille or braille monitor"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        target = current_script or self
        if target is not self:
            return target.pan_braille_left(target, event)

        tokens = ["DEFAULT: pan_braille_left using", target]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        return self._pan_braille_left(event)

    def _pan_braille_left(self, event: input_event.InputEvent | None = None) -> bool:
        """Pans the braille display to the left."""

        if flat_review_presenter.get_presenter().is_active():
            return flat_review_presenter.get_presenter().pan_braille_left(self, event)

        presenter = braille_presenter.get_presenter()
        if presenter.pan_left():
            return True

        # Couldn't pan (at edge). For text areas, move caret to get more content.
        focus = focus_manager.get_manager().get_locus_of_focus()
        is_text_area = AXUtilities.is_editable(focus) or AXUtilities.is_terminal(focus)
        if not is_text_area:
            return True

        start_offset = AXText.get_line_at_offset(focus)[1]
        moved_caret = False
        if start_offset > 0:
            moved_caret = AXText.set_caret_offset(focus, start_offset - 1)

        # If we didn't move the caret and we're in a terminal, we
        # jump into flat review to review the text.  See
        # http://bugzilla.gnome.org/show_bug.cgi?id=482294.
        if not moved_caret and AXUtilities.is_terminal(focus):
            flat_review_presenter.get_presenter().go_start_of_line(self, event)
            flat_review_presenter.get_presenter().go_previous_character(self, event)

        return True

    def pan_braille_right(
        self,
        current_script: Script | None = None,
        event: input_event.InputEvent | None = None,
    ) -> bool:
        """Pans the braille display to the right."""

        if (
            isinstance(event, input_event.KeyboardEvent)
            and not braille_presenter.get_presenter().use_braille()
        ):
            msg = "DEFAULT: panBrailleRight command requires braille or braille monitor"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        target = current_script or self
        if target is not self:
            return target.pan_braille_right(target, event)

        tokens = ["DEFAULT: pan_braille_right using", target]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        return self._pan_braille_right(event)

    def _pan_braille_right(self, event: input_event.InputEvent | None = None) -> bool:
        """Pans the braille display to the right."""

        if flat_review_presenter.get_presenter().is_active():
            return flat_review_presenter.get_presenter().pan_braille_right(self, event)

        presenter = braille_presenter.get_presenter()
        if presenter.pan_right():
            return True

        # Couldn't pan (at edge). For text areas, move caret to get more content.
        focus = focus_manager.get_manager().get_locus_of_focus()
        is_text_area = AXUtilities.is_editable(focus) or AXUtilities.is_terminal(focus)
        if not is_text_area:
            return True

        end_offset = AXText.get_line_at_offset(focus)[2]
        if end_offset < AXText.get_character_count(focus):
            AXText.set_caret_offset(focus, end_offset)

        return True

    def go_braille_home(
        self,
        _script: Script | None = None,
        event: input_event.InputEvent | None = None,
    ) -> bool:
        """Returns to the component with focus."""

        if flat_review_presenter.get_presenter().is_active():
            flat_review_presenter.get_presenter().quit()
            return True

        presentation_manager.get_manager().interrupt_presentation()
        return braille.return_to_region_with_focus(event)

    def set_contracted_braille(
        self,
        _script: Script | None = None,
        event: input_event.InputEvent | None = None,
    ) -> bool:
        """Toggles contracted braille."""

        braille.toggle_contracted_braille(event)
        return True

    def process_routing_key(
        self,
        _script: Script | None = None,
        event: input_event.BrailleEvent | None = None,
    ) -> bool:
        """Processes a cursor routing key."""

        # Don't kill flash here because it will restore the previous contents and
        # then process the routing key. If the contents accept a click action, this
        # would result in clicking on the link instead of clearing the flash message.
        presentation_manager.get_manager().interrupt_presentation(kill_flash=False)
        if event is None:
            return True
        braille.process_routing_key(event)
        return True

    def process_braille_cut_begin(
        self,
        _script: Script | None = None,
        event: input_event.BrailleEvent | None = None,
    ) -> bool:
        """Clears the selection and moves the caret offset in the currently
        active text area.
        """

        if event is None:
            return True
        caret_context = braille.get_caret_context(event)
        if caret_context.offset < 0:
            return True

        presentation_manager.get_manager().interrupt_presentation()
        AXUtilities.clear_all_selected_text(caret_context.accessible)
        self.utilities.set_caret_offset(caret_context.accessible, caret_context.offset)
        return True

    def process_braille_cut_line(
        self,
        _script: Script | None = None,
        event: input_event.BrailleEvent | None = None,
    ) -> bool:
        """Extends the text selection in the currently active text
        area and also copies the selected text to the system clipboard."""

        if event is None:
            return True
        caret_context = braille.get_caret_context(event)
        if caret_context.offset < 0:
            return True

        presentation_manager.get_manager().interrupt_presentation()
        start_offset = AXUtilities.get_selection_start_offset(caret_context.accessible)
        end_offset = AXUtilities.get_selection_end_offset(caret_context.accessible)
        if start_offset < 0 or end_offset < 0:
            caret_offset = AXText.get_caret_offset(caret_context.accessible)
            start_offset = min(caret_context.offset, caret_offset)
            end_offset = max(caret_context.offset, caret_offset)

        AXUtilities.set_selected_text(caret_context.accessible, start_offset, end_offset)
        text = AXUtilities.get_selected_text(caret_context.accessible)[0]
        clipboard.get_presenter().set_text(text)
        return True

    def route_pointer_to_item(
        self,
        _script: Script | None = None,
        event: input_event.InputEvent | None = None,
    ) -> bool:
        """Moves the mouse pointer to the current item."""

        if flat_review_presenter.get_presenter().is_active():
            flat_review_presenter.get_presenter().route_pointer_to_object(self, event)
            return True

        focus = focus_manager.get_manager().get_locus_of_focus()
        if ax_event_synthesizer.get_synthesizer().route_to_character(
            focus,
        ) or ax_event_synthesizer.get_synthesizer().route_to_object(focus):
            presentation_manager.get_manager().present_message(messages.MOUSE_MOVED_SUCCESS)
            return True

        full = messages.LOCATION_NOT_FOUND_FULL
        brief = messages.LOCATION_NOT_FOUND_BRIEF
        presentation_manager.get_manager().present_message(full, brief)
        return False

    def left_click_item(
        self,
        _script: Script | None = None,
        event: input_event.InputEvent | None = None,
    ) -> bool:
        """Performs a left mouse button click on the current item."""

        if flat_review_presenter.get_presenter().is_active():
            obj = flat_review_presenter.get_presenter().get_current_object(self, event)
            if ax_event_synthesizer.get_synthesizer().try_all_clickable_actions(obj):
                return True
            return flat_review_presenter.get_presenter().left_click_on_object(self, event)

        focus = focus_manager.get_manager().get_locus_of_focus()
        if ax_event_synthesizer.get_synthesizer().try_all_clickable_actions(focus):
            return True

        if AXText.get_character_count(focus):
            if ax_event_synthesizer.get_synthesizer().click_character(focus, None, 1):
                return True

        if ax_event_synthesizer.get_synthesizer().click_object(focus, 1):
            return True

        full = messages.LOCATION_NOT_FOUND_FULL
        brief = messages.LOCATION_NOT_FOUND_BRIEF
        presentation_manager.get_manager().present_message(full, brief)
        return False

    def right_click_item(
        self,
        _script: Script | None = None,
        event: input_event.InputEvent | None = None,
    ) -> bool:
        """Performs a right mouse button click on the current item."""

        if flat_review_presenter.get_presenter().is_active():
            obj = flat_review_presenter.get_presenter().get_current_object(self, event)
            if ax_event_synthesizer.get_synthesizer().try_all_right_click_actions(obj):
                return True
            flat_review_presenter.get_presenter().right_click_on_object(self, event)
            return True

        focus = focus_manager.get_manager().get_locus_of_focus()
        if ax_event_synthesizer.get_synthesizer().try_all_right_click_actions(focus):
            return True

        if ax_event_synthesizer.get_synthesizer().click_character(focus, None, 3):
            return True

        if ax_event_synthesizer.get_synthesizer().click_object(focus, 3):
            return True

        full = messages.LOCATION_NOT_FOUND_FULL
        brief = messages.LOCATION_NOT_FOUND_BRIEF
        presentation_manager.get_manager().present_message(full, brief)
        return False

    ########################################################################
    #                                                                      #
    # AT-SPI OBJECT EVENT HANDLERS                                         #
    #                                                                      #
    # Event handlers return bool:                                          #
    # - True: Event was fully handled, no further processing needed        #
    # - False: Event wasn't handled, should be passed to parent handlers   #
    # Default return value is True (event handled by default script)       #
    #                                                                      #
    ########################################################################

    def _on_active_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:active accessibility events."""

        window = event.source
        if AXUtilities.is_dialog_or_alert(window) or AXUtilities.is_frame(window):
            if event.detail1 and not AXUtilities.can_be_active_window(window):
                return True

            source_is_active_window = window == focus_manager.get_manager().get_active_window()
            if source_is_active_window and not event.detail1:
                focus = focus_manager.get_manager().get_locus_of_focus()
                if AXUtilities.find_ancestor_inclusive(focus, AXUtilities.is_menu):
                    msg = "DEFAULT: Ignoring event. In menu."
                    debug.print_message(debug.LEVEL_INFO, msg, True)
                    return True

                msg = "DEFAULT: Event is for active window. Clearing state."
                debug.print_message(debug.LEVEL_INFO, msg, True)
                focus_manager.get_manager().set_active_window(None)
                return True

            if not source_is_active_window and event.detail1:
                msg = "DEFAULT: Updating active window."
                debug.print_message(debug.LEVEL_INFO, msg, True)
                focus_manager.get_manager().set_active_window(
                    window,
                    set_window_as_focus=True,
                    notify_script=True,
                )

        return True

    def _on_active_descendant_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:active-descendant-changed accessibility events."""

        if AXUtilities.is_presentable_active_descendant_change(event):
            focus_manager.get_manager().set_locus_of_focus(event, event.any_data)

        return True

    def _on_checked_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:checked accessibility events."""

        if AXUtilities.is_presentable_checked_change(event):
            self.present_object(event.source, alreadyFocused=True, interrupt=True)

        return True

    def _on_children_added(self, event: Atspi.Event) -> bool:
        """Callback for object:children-changed:add accessibility events."""

        AXUtilities.clear_all_cache_now(event.source, "children-changed event.")
        return True

    def _on_children_removed(self, event: Atspi.Event) -> bool:
        """Callback for object:children-changed:remove accessibility events."""

        AXUtilities.clear_all_cache_now(event.source, "children-changed event.")
        return True

    # pylint: disable-next=too-many-return-statements
    def _on_caret_moved(self, event: Atspi.Event) -> bool:
        """Callback for object:text-caret-moved accessibility events."""

        reason = AXUtilities.get_text_event_reason(event)
        if reason == TextEventReason.SEARCH_PRESENTABLE:
            msg = "DEFAULT: Presenting line for search result change"
            contents = self.utilities.get_line_contents_at_offset(event.source, event.detail1)
            presentation_manager.get_manager().speak_contents(contents)
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        manager = focus_manager.get_manager()
        focus = manager.get_locus_of_focus()
        if focus != event.source:
            if not AXUtilities.is_focused(event.source):
                msg = "DEFAULT: Change is from unfocused source that is not the locus of focus"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True
            # TODO - JD: See if this can be removed. If it's still needed document why.
            manager.set_locus_of_focus(event, event.source, False)

        obj, offset = manager.get_last_cursor_position()
        if offset == event.detail1 and obj == event.source:
            navigation_reasons = {
                TextEventReason.NAVIGATION_BY_WORD,
                TextEventReason.NAVIGATION_BY_CHARACTER,
            }
            if reason not in navigation_reasons:
                msg = "DEFAULT: Event is for last saved cursor position"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True
            msg = "DEFAULT: Position matches but proceeding due to navigation reason"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            presentation_manager.get_manager().interrupt_presentation()

        if flat_review_presenter.get_presenter().is_active():
            flat_review_presenter.get_presenter().quit()

        offset = AXText.get_caret_offset(event.source)

        # TODO - JD: These need to be harmonized / unified / simplified.
        manager.set_last_cursor_position(event.source, offset)
        self.utilities.set_caret_context(event.source, offset)

        ignore = [
            TextEventReason.CUT,
            TextEventReason.PASTE,
            TextEventReason.REDO,
            TextEventReason.UNDO,
        ]
        if reason in ignore:
            msg = f"DEFAULT: Ignoring event due to reason ({reason})"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            AXUtilities.update_cached_selected_text(event.source)
            return True

        if AXUtilities.has_selected_text(event.source):
            msg = "DEFAULT: Event source has text selections"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self.utilities.handle_text_selection_change(event.source)
            return True

        text, _start, _end = AXUtilities.get_cached_selected_text(obj)
        if text and self.utilities.handle_text_selection_change(obj):
            msg = "DEFAULT: Event handled as text selection change"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        msg = "DEFAULT: Presenting text at new caret position"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._present_caret_moved_event(event, reason=reason)
        return True

    def _on_description_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:property-change:accessible-description events."""

        if AXUtilities.is_presentable_description_change(event):
            presentation_manager.get_manager().present_message(event.any_data)
        return True

    def _on_document_page_changed(self, event: Atspi.Event) -> bool:
        """Callback for document:page-changed accessibility events."""

        if event.detail1 < 0:
            return True

        if not AXDocument.did_page_change(event.source):
            return True

        presentation_manager.get_manager().present_message(messages.PAGE_NUMBER % event.detail1)
        return True

    def _on_expanded_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:expanded accessibility events."""

        AXUtilities.clear_all_cache_now(event.source, "expanded-changed event.")
        if not AXUtilities.is_presentable_expanded_change(event):
            return True

        self.present_object(event.source, alreadyFocused=True, interrupt=True)
        details = AXUtilities.get_details_content(event.source)
        for detail in details:
            presentation_manager.get_manager().speak_message(detail)

        return True

    def _on_indeterminate_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:indeterminate accessibility events."""

        if AXUtilities.is_presentable_indeterminate_change(event):
            self.present_object(event.source, alreadyFocused=True, interrupt=True)

        return True

    def _on_invalid_entry_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:invalid-entry accessibility events."""

        if not AXUtilities.is_presentable_invalid_entry_change(event):
            return True

        if event.detail1:
            msg = self.get_speech_generator().get_error_message(event.source)
        else:
            msg = messages.INVALID_ENTRY_FIXED
        presentation_manager.get_manager().speak_message(msg)
        self.update_braille(event.source)
        return True

    def _on_mouse_button(self, event: Atspi.Event) -> bool:
        """Callback for mouse:button events."""

        input_event_manager.get_manager().process_mouse_button_event(event)
        return True

    def _on_announcement(self, event: Atspi.Event) -> bool:
        """Callback for object:announcement events."""

        if isinstance(event.any_data, str):
            presentation_manager.get_manager().present_message(event.any_data)

        return True

    def _on_name_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:property-change:accessible-name events."""

        if not AXUtilities.is_presentable_name_change(event):
            return True

        manager = focus_manager.get_manager()
        if event.source == manager.get_locus_of_focus():
            # Force the update so that braille is refreshed.
            manager.set_locus_of_focus(event, event.source, True, True)
            return True

        presentation_manager.get_manager().present_message(event.any_data)
        return True

    def _on_object_attributes_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:attributes-changed accessibility events."""

        AXUtilities.clear_all_cache_now(event.source, "object-attributes-changed event.")
        return True

    def _on_pressed_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:pressed accessibility events."""

        if AXUtilities.is_presentable_pressed_change(event):
            self.present_object(event.source, alreadyFocused=True, interrupt=True)

        return True

    def _on_selected_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:selected accessibility events."""

        if not AXUtilities.is_presentable_selected_change(event):
            return True

        if speech_presenter.get_presenter().get_only_speak_displayed_text():
            return True

        announce_state = False
        manager = input_event_manager.get_manager()
        if manager.last_event_was_space():
            announce_state = True
        elif (
            manager.last_event_was_up() or manager.last_event_was_down()
        ) and AXUtilities.is_table_cell(event.source):
            announce_state = AXUtilities.is_selected(event.source)

        if not announce_state:
            return True

        # TODO - JD: Unlike the other state-changed callbacks, it seems unwise
        # to call generate_speech() here because that also will present the
        # expandable state if appropriate for the object type. The generators
        # need to gain some smarts w.r.t. state changes.

        if event.detail1:
            presentation_manager.get_manager().speak_message(messages.TEXT_SELECTED)
        else:
            presentation_manager.get_manager().speak_message(messages.TEXT_UNSELECTED)

        return True

    def _on_selection_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:selection-changed accessibility events."""

        if not AXUtilities.is_presentable_selection_change(event):
            return True

        presentation_manager.get_manager().present_command_announcement()

        if self.utilities.handle_container_selection_change(event.source):
            return True

        if (
            AXUtilities.is_page_tab_list(event.source)
            and flat_review_presenter.get_presenter().is_active()
        ):
            # If a wizard-like notebook page being reviewed changes, we might not get
            # any events to update the locusOfFocus. As a result, subsequent flat
            # review commands will continue to present the stale content.
            # TODO - JD: We can potentially do some automatic reading here.
            flat_review_presenter.get_presenter().quit()

        focus = focus_manager.get_manager().get_locus_of_focus()
        mouse_review_item = mouse_review.get_reviewer().get_current_item()
        child = AXUtilities.get_selected_child_for_focus(
            event.source,
            focus,
            lambda c: c == mouse_review_item or AXUtilities.is_layout_only(c),
        )
        if child is not None:
            focus_manager.get_manager().set_locus_of_focus(event, child)

        return True

    def _on_focused_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:focused accessibility events."""

        if not event.detail1:
            return True

        if not AXUtilities.is_focused(event.source):
            tokens = ["DEFAULT:", event.source, "lacks focused state. Clearing cache."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            AXObject.clear_cache(event.source, reason="Event detail1 does not match state.")
            if not AXUtilities.is_focused(event.source):
                msg = "DEFAULT: Clearing cache did not update state."
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

        obj = event.source
        window, dialog = self.utilities.frame_and_dialog(obj)
        if window and not AXUtilities.can_be_active_window(window) and not dialog:
            return True

        if AXObject.get_child_count(obj) and not AXUtilities.is_combo_box(obj):
            selected_children = AXUtilities.selected_children(obj)
            if selected_children:
                obj = selected_children[0]

        focus_manager.get_manager().set_locus_of_focus(event, obj)
        return True

    def _on_showing_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:state-changed:showing accessibility events."""

        obj = event.source
        if AXUtilities.is_notification(obj):
            if not event.detail1:
                return True

            presentation_manager.get_manager().speak_message(
                self.get_speech_generator().get_localized_role_name(obj),
            )
            msg = self.utilities.get_notification_content(obj)
            presenter = presentation_manager.get_manager()
            presenter.speak_accessible_text(obj, msg)
            presenter.present_braille_message(msg)
            notification_presenter.get_presenter().save_notification(msg)
            return True

        if AXUtilities.is_tool_tip(obj):
            was_f1 = input_event_manager.get_manager().last_event_was_f1()
            if not was_f1 and not mouse_review.get_reviewer().get_present_tooltips():
                return True
            if event.detail1:
                self.present_object(obj, interrupt=True)
                return True

            focus = focus_manager.get_manager().get_locus_of_focus()
            if focus and was_f1:
                obj = focus
                self.present_object(obj, priorObj=event.source, interrupt=True)
                return True

        return True

    def _on_text_attributes_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:text-attributes-changed accessibility events."""

        if not AXUtilities.is_presentable_text_attributes_change(event):
            return True

        word, start, _end = AXText.get_word_at_offset(event.source)
        if not word.strip():
            word, start, _end = AXText.get_word_at_offset(event.source, start - 1)

        speech_pres = speech_presenter.get_presenter()
        if error := speech_pres.get_error_description(event.source, start, False):
            presentation_manager.get_manager().speak_message(error)

        return True

    def _on_text_deleted(self, event: Atspi.Event) -> bool:
        """Callback for object:text-changed:delete accessibility events."""

        if not AXUtilities.is_presentable_text_deletion(event):
            return True

        reason = AXUtilities.get_text_event_reason(event)
        presentation_manager.get_manager().present_command_announcement()
        self.update_braille(event.source)

        if reason == TextEventReason.SELECTED_TEXT_DELETION:
            msg = "DEFAULT: Deletion is believed to be due to deleting selected text"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            presentation_manager.get_manager().present_message(messages.SELECTION_DELETED)
            AXUtilities.update_cached_selected_text(event.source)
            return True

        text = self.utilities.deleted_text(event)
        selected_text, _start, _end = AXUtilities.get_cached_selected_text(event.source)
        if reason == TextEventReason.DELETE:
            msg = "DEFAULT: Deletion is believed to be due to Delete command"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            text = AXText.get_character_at_offset(event.source)[0]
        elif reason == TextEventReason.BACKSPACE and text != selected_text:
            msg = "DEFAULT: Deletion is believed to be due to BackSpace command"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        else:
            msg = "DEFAULT: Event is not being presented due to lack of cause"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if len(text) == 1:
            presentation_manager.get_manager().speak_character(text)
        else:
            presentation_manager.get_manager().speak_accessible_text(event.source, text)

        return True

    def _on_text_inserted(self, event: Atspi.Event) -> bool:
        """Callback for object:text-changed:insert accessibility events."""

        if not AXUtilities.is_presentable_text_insertion(event):
            return True

        reason = AXUtilities.get_text_event_reason(event)
        presentation_manager.get_manager().present_command_announcement()
        self.update_braille(event.source)

        if reason == TextEventReason.SELECTED_TEXT_RESTORATION:
            msg = "DEFAULT: Insertion is believed to be due to restoring selected text"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            presentation_manager.get_manager().present_message(messages.SELECTION_RESTORED)
            AXUtilities.update_cached_selected_text(event.source)
            return True

        if reason == TextEventReason.LIVE_REGION_UPDATE:
            msg = "DEFAULT: Event is from live region"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            live_region_presenter.get_presenter().handle_event(self, event)
            return True

        reason_messages: dict[TextEventReason, str] = {
            TextEventReason.PAGE_SWITCH: "due to page switch",
            TextEventReason.PASTE: "due to paste",
            TextEventReason.UNSPECIFIED_COMMAND: "due to command",
            TextEventReason.MOUSE_MIDDLE_BUTTON: "due to middle mouse button",
            TextEventReason.TYPING_ECHOABLE: "echoable",
            TextEventReason.AUTO_INSERTION_PRESENTABLE: "presentable auto text event",
            TextEventReason.SELECTED_TEXT_INSERTION: "also selected",
        }
        silent_reasons = {TextEventReason.PAGE_SWITCH, TextEventReason.PASTE}
        description = reason_messages.get(reason)
        if description is not None:
            msg = f"DEFAULT: Insertion is believed to be {description}"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            speak_string = reason not in silent_reasons
        else:
            msg = "DEFAULT: Not speaking inserted string due to lack of cause"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            speak_string = False

        # Because some implementations are broken.
        text = self.utilities.inserted_text(event)
        if speak_string:
            if len(text) == 1:
                presentation_manager.get_manager().speak_character(text)
            else:
                presentation_manager.get_manager().speak_accessible_text(event.source, text)

        if len(text) != 1 or reason not in [
            TextEventReason.TYPING,
            TextEventReason.TYPING_ECHOABLE,
        ]:
            return True

        if AXText.get_selected_ranges(event.source):
            return True

        presenter = typing_echo_presenter.get_presenter()
        if presenter.echo_previous_sentence(event.source):
            return True

        presenter.echo_previous_word(event.source)
        return True

    def _on_text_selection_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:text-selection-changed accessibility events."""

        if AXUtilities.is_focusable(event.source) and not AXUtilities.is_focused(event.source):
            msg = "DEFAULT: Ignoring event from focusable but unfocused source"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        # We won't handle undo here as it can lead to double-presentation.
        # If there is an application for which text-changed events are
        # missing upon undo, handle them in an app or toolkit script.

        reason = AXUtilities.get_text_event_reason(event)
        if reason == TextEventReason.UNKNOWN:
            msg = "DEFAULT: Ignoring event because reason for change is unknown"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            AXUtilities.update_cached_selected_text(event.source)
            return True
        if reason == TextEventReason.SEARCH_PRESENTABLE:
            msg = "DEFAULT: Presenting line believed to be search match"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self.say_line(event.source)
            AXUtilities.update_cached_selected_text(event.source)
            return True
        if reason == TextEventReason.SEARCH_UNPRESENTABLE:
            msg = "DEFAULT: Ignoring event believed to be unpresentable search results change"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            AXUtilities.update_cached_selected_text(event.source)
            return True
        if reason in [TextEventReason.CUT, TextEventReason.BACKSPACE, TextEventReason.DELETE]:
            msg = "DEFAULT: Ignoring event believed to be text removal"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            AXUtilities.update_cached_selected_text(event.source)
            return True
        if reason == TextEventReason.AUTO_SELECTION:
            msg = "DEFAULT: Ignoring auto-selection believed to be side effect of typing"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            AXUtilities.update_cached_selected_text(event.source)
            return True
        if reason == TextEventReason.AUTO_UNSELECTION:
            msg = "DEFAULT: Ignoring auto-unselection believed to be side effect of typing"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            AXUtilities.update_cached_selected_text(event.source)
            return True

        self.utilities.handle_text_selection_change(event.source)
        self.update_braille(event.source)
        return True

    def _on_column_reordered(self, event: Atspi.Event) -> bool:
        """Callback for object:column-reordered accessibility events."""

        AXUtilities.clear_all_cache_now(event.source, "column-reordered event.")
        if not input_event_manager.get_manager().last_event_was_table_sort():
            return True

        if event.source != AXUtilities.get_table(focus_manager.get_manager().get_locus_of_focus()):
            return True

        presentation_manager.get_manager().present_message(messages.TABLE_REORDERED_COLUMNS)
        return True

    def _on_row_reordered(self, event: Atspi.Event) -> bool:
        """Callback for object:row-reordered accessibility events."""

        AXUtilities.clear_all_cache_now(event.source, "row-reordered event.")
        if not input_event_manager.get_manager().last_event_was_table_sort():
            return True

        if event.source != AXUtilities.get_table(focus_manager.get_manager().get_locus_of_focus()):
            return True

        presentation_manager.get_manager().present_message(messages.TABLE_REORDERED_ROWS)
        return True

    def _on_value_changed(self, event: Atspi.Event) -> bool:
        """Callback for object:property-change:accessible-value accessibility events."""

        if not AXUtilities.is_presentable_value_change(event):
            return True

        manager = focus_manager.get_manager()
        if (
            not AXUtilities.is_progress_bar(event.source)
            and event.source != manager.get_locus_of_focus()
        ):
            msg = "DEFAULT: Source != locusOfFocus"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.is_spin_button(event.source):
            manager.set_last_cursor_position(event.source, AXText.get_caret_offset(event.source))

        if not AXUtilities.is_progress_bar(event.source):
            presentation_manager.get_manager().interrupt_presentation()

        presentation_manager.get_manager().present_object(
            self,
            event.source,
            generate_sound=True,
            alreadyFocused=True,
            isProgressBarUpdate=AXUtilities.is_progress_bar(event.source),
        )
        return True

    def _on_window_activated(self, event: Atspi.Event) -> bool:
        """Callback for window:activate accessibility events."""

        if not AXUtilities.can_be_active_window(event.source):
            return True

        manager = focus_manager.get_manager()
        active_window = manager.get_active_window()
        if event.source == active_window:
            msg = "DEFAULT: Event is for active window."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            spellcheck_presenter.get_presenter().handle_window_event(event, self)
            return True

        manager.set_active_window(event.source)
        if AXUtilities.is_combo_box_popup(active_window):
            return True

        if AXObject.get_child_count(event.source) == 1:
            child = AXObject.get_child(event.source, 0)
            # Popup menus in Chromium live in a menu bar whose first child is a panel.
            if AXUtilities.is_menu_bar(child):
                child = AXUtilities.get_menu(child)
            if AXUtilities.is_menu(child):
                selected_item = AXSelection.get_selected_child(child, 0)
                if AXUtilities.is_selected(selected_item):
                    child = selected_item
                manager.set_locus_of_focus(event, child)
                return True

        if not spellcheck_presenter.get_presenter().handle_window_event(event, self):
            manager.set_locus_of_focus(event, event.source)
        return True

    def _on_window_deactivated(self, event: Atspi.Event) -> bool:
        """Callback for window:deactivate accessibility events."""

        manager = focus_manager.get_manager()
        focus = manager.get_locus_of_focus()
        if AXUtilities.find_ancestor_inclusive(focus, AXUtilities.is_menu):
            msg = "DEFAULT: Ignoring event. In menu."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if event.source != manager.get_active_window():
            msg = "DEFAULT: Ignoring event. Not for active window"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.is_combo_box_popup(AXUtilities.find_active_window()):
            msg = "DEFAULT: Ignoring event. Combo box popup is the new active window."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.is_combo_box_popup(event.source):
            msg = "DEFAULT: Ignoring event. Combo box popup."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if flat_review_presenter.get_presenter().is_active():
            flat_review_presenter.get_presenter().quit()

        if learn_mode_presenter.get_presenter().is_active():
            learn_mode_presenter.get_presenter().quit()

        focus_manager.get_manager().clear_state("Window deactivated")
        script_manager.get_manager().set_active_script(None, "Window deactivated")
        return True

    ########################################################################
    #                                                                      #
    # Methods for presenting content                                       #
    #                                                                      #
    ########################################################################

    def _update_braille_caret_position(self, obj: Atspi.Accessible) -> None:
        """Try to reposition the cursor without having to do a full update."""

        if not braille_presenter.get_presenter().use_braille():
            return

        if braille.try_reposition_cursor(obj):
            return

        self.update_braille(obj)

    def _present_caret_moved_event(
        self,
        event: Atspi.Event,
        obj: Atspi.Accessible | None = None,
        reason: TextEventReason = TextEventReason.UNKNOWN,
    ) -> bool:
        """Presents text at the new position, based on heuristics. Returns True if handled."""

        obj = obj or event.source
        self._update_braille_caret_position(obj)

        # TODO - JD: Make this a TextEventReason. Also handle structural navigation
        # and table navigation here. Technically that's not been necessary because
        # it won't match anything below. But it would be cleaner to cover each cause
        # explicitly.
        if caret_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "DEFAULT: Event ignored: Last command was caret nav"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if reason == TextEventReason.SAY_ALL:
            msg = "DEFAULT: Not presenting text because SayAll is active"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        navigation_handlers: dict[TextEventReason, Callable[[Atspi.Accessible], None]] = {
            TextEventReason.NAVIGATION_BY_LINE: self.say_line,
            TextEventReason.NAVIGATION_BY_WORD: self.say_word,
            TextEventReason.NAVIGATION_BY_CHARACTER: self.say_character,
            TextEventReason.NAVIGATION_BY_PAGE: self.say_line,
            TextEventReason.NAVIGATION_TO_LINE_BOUNDARY: self.say_character,
            TextEventReason.NAVIGATION_TO_FILE_BOUNDARY: self.say_line,
        }
        handler = navigation_handlers.get(reason)
        if handler is not None:
            handler(obj)
            return True

        if reason == TextEventReason.MOUSE_PRIMARY_BUTTON:
            text, _start, _end = AXUtilities.get_cached_selected_text(event.source)
            if not text:
                self.say_line(obj)
                return True
        return False

    def say_character(self, obj: Atspi.Accessible) -> None:
        """Speak the character at the caret."""

        context_obj, context_offset = self.utilities.get_caret_context(obj)
        if context_obj == obj:
            offset = context_offset
        else:
            offset = AXText.get_caret_offset(obj)

        if input_event_manager.get_manager().last_event_was_forward_caret_selection():
            offset -= 1

        character, start_offset, end_offset = AXText.get_character_at_offset(obj, offset)
        focus_manager.get_manager().emit_region_changed(
            obj,
            start_offset,
            end_offset,
            focus_manager.CARET_TRACKING,
        )

        presentation_manager.get_manager().speak_character_at_offset(obj, offset, character)
        AXUtilities.set_last_text_unit_spoken(TextUnit.CHAR)

    def say_line(self, obj: Atspi.Accessible, offset: int | None = None) -> None:
        """Speaks the line at the current or specified offset."""

        if offset is None:
            offset = AXText.get_caret_offset(obj)
        else:
            AXText.set_caret_offset(obj, offset)

        line, start_offset = AXText.get_line_at_offset(obj, offset)[0:2]
        if line and line != "\n":
            end_offset = start_offset + len(line)
            focus_manager.get_manager().emit_region_changed(
                obj,
                start_offset,
                end_offset,
                focus_manager.CARET_TRACKING,
            )

        speech_presenter.get_presenter().speak_line(
            self,
            obj,
            start_offset,
            start_offset + len(line),
            line,
        )

        AXUtilities.set_last_text_unit_spoken(TextUnit.LINE)

    def say_phrase(self, obj: Atspi.Accessible, start_offset: int, end_offset: int) -> None:
        """Speaks the substring between start and end offset."""

        phrase = self.utilities.expand_eocs(obj, start_offset, end_offset)
        if not phrase:
            return

        if len(phrase) > 1 or phrase.isalnum():
            focus_manager.get_manager().emit_region_changed(
                obj,
                start_offset,
                end_offset,
                focus_manager.CARET_TRACKING,
            )

        speech_presenter.get_presenter().speak_phrase(self, obj, start_offset, end_offset, phrase)
        AXUtilities.set_last_text_unit_spoken(TextUnit.PHRASE)

    def say_word(self, obj: Atspi.Accessible) -> None:
        """Speaks the word at the caret, taking into account the previous caret position."""

        context_obj, context_offset = self.utilities.get_caret_context(obj)
        if context_obj == obj:
            offset = context_offset
        else:
            offset = AXText.get_caret_offset(obj)

        word, start_offset, end_offset = self.utilities.get_word_at_offset_adjusted_for_navigation(
            obj,
            offset,
        )

        speech_pres = speech_presenter.get_presenter()
        if "\n" in word:
            # Announce when we cross a hard line boundary, based on whether or not indentation and
            # justification should be spoken. This was done to avoid yet another setting in
            # response to some users saying this announcement was too chatty. The idea of using
            # this setting for the decision is that if the user wants indentation and justification
            # announced, they are interested in explicit whitespace information.
            if speech_pres.get_speak_indentation_and_justification():
                presentation_manager.get_manager().speak_character("\n")
            if word.startswith("\n"):
                start_offset += 1
            elif word.endswith("\n"):
                end_offset -= 1
            word = AXText.get_substring(obj, start_offset, end_offset)

        # say_phrase is useful because it handles punctuation verbalization, but we don't want
        # to trigger its whitespace presentation.
        matches = list(re.finditer(r"\S+", word))
        if matches:
            start_offset += matches[0].start()
            end_offset -= len(word) - matches[-1].end()
            word = AXText.get_substring(obj, start_offset, end_offset)

        text = word.replace("\n", "\\n")
        msg = f"DEFAULT: Final word at offset {offset} is '{text}' ({start_offset}-{end_offset})"
        debug.print_message(debug.LEVEL_INFO, msg, True)

        if error := speech_presenter.get_presenter().get_error_description(obj, start_offset):
            presentation_manager.get_manager().speak_message(error)

        self.say_phrase(obj, start_offset, end_offset)
        AXUtilities.set_last_text_unit_spoken(TextUnit.WORD)

    def present_object(self, obj: Atspi.Accessible, **args) -> None:
        """Presents the current object."""

        tokens = ["DEFAULT: Presenting object", obj, ". Interrupt:", args.get("interrupt", False)]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        offset = args.get("offset")
        if offset is not None:
            AXText.set_caret_offset(obj, offset)

        speech_only = args.pop("speechonly", False)
        presentation_manager.get_manager().present_object(
            self,
            obj,
            generate_braille=not speech_only,
            **args,
        )
