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

# pylint: disable=too-many-boolean-expressions
# pylint: disable=too-many-branches
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-lines
# pylint: disable=too-many-locals
# pylint: disable=too-many-return-statements
# pylint: disable=too-many-statements

"""Provides support for accessing user-agent-agnostic web-content."""

from __future__ import annotations

from typing import TYPE_CHECKING

from orca import (
    braille_presenter,
    caret_navigator,
    debug,
    document_presenter,
    flat_review_presenter,
    focus_manager,
    input_event,
    input_event_manager,
    label_inference,
    live_region_presenter,
    messages,
    presentation_manager,
    say_all_presenter,
    speech_manager,
    speech_presenter,
    structural_navigator,
    table_navigator,
)
from orca.ax_document import AXDocument
from orca.ax_event_synthesizer import AXEventSynthesizer
from orca.ax_object import AXObject
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
from orca.scripts import default
from orca.structural_navigator import NavigationMode

from .braille_generator import BrailleGenerator
from .script_utilities import Utilities
from .speech_generator import SpeechGenerator

if TYPE_CHECKING:
    from gi.repository import Atspi


class Script(default.Script):
    """Provides support for accessing user-agent-agnostic web-content."""

    # Type annotations to override the base class types
    utilities: Utilities
    caret_navigator: caret_navigator.CaretNavigator

    def __init__(self, app: Atspi.Accessible) -> None:
        super().__init__(app)

        self.present_if_inactive: bool = False
        self._default_sn_mode = NavigationMode.DOCUMENT
        self._default_caret_navigation_enabled: bool = True

        self._loading_content = False
        self._last_mouse_button_context = None, -1

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

        self._loading_content = False
        document_presenter.get_presenter().reset_find_announcement_state()
        self._last_mouse_button_context = None, -1
        self.utilities.clear_cached_objects()
        super().deactivate()

    def _create_braille_generator(self) -> BrailleGenerator:
        """Creates and returns the braille generator for this script."""

        return BrailleGenerator(self)

    def get_label_inference(self) -> label_inference.LabelInference | None:
        """Returns the label inference functionality for this script."""

        return label_inference.LabelInference(self)

    def _create_speech_generator(self) -> SpeechGenerator:
        """Creates and returns the speech generator for this script."""

        return SpeechGenerator(self)

    def get_utilities(self) -> Utilities:
        """Returns the utilities for this script."""

        return Utilities(self)

    def is_loading_content(self) -> bool:
        """Returns True if we're currently loading content."""

        return self._loading_content

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

        tokens = ["WEB: Say character for", obj]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        if not self.utilities.in_document_content(obj):
            msg = "WEB: Object is not in document content."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            super().say_character(obj)
            return

        document = self.utilities.get_top_level_document_for_object(obj)
        obj, offset = self.utilities.get_caret_context(document=document)
        tokens = ["WEB: Adjusted object and offset for say character to", obj, offset]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if not obj:
            return

        contents: list[tuple[Atspi.Accessible, int, int, str]] | None = None
        if self.utilities.treat_as_end_of_line(obj, offset) and AXObject.supports_text(obj):
            char = AXText.get_character_at_offset(obj, offset)[0]
            if char == "\ufffc":
                char = ""
            contents = [(obj, offset, offset + 1, char)]
        else:
            contents = self.utilities.get_character_contents_at_offset(obj, offset)

        if not contents:
            return

        speech_pres = speech_presenter.get_presenter()
        obj, start, _end, string = contents[0]
        if start > 0 and string == "\n":
            if speech_pres.get_speak_blank_lines():
                presentation_manager.get_manager().speak_message(messages.BLANK)
                return

        presenter = presentation_manager.get_manager()
        if string:
            if error := speech_pres.get_error_description(obj, start):
                presenter.speak_message(error)
            presenter.speak_character(string, obj=obj)
        else:
            presenter.speak_contents(contents)

        AXUtilities.set_last_text_unit_spoken(TextUnit.CHAR)

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

        tokens = ["WEB: Say word for", obj]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        if not self.utilities.in_document_content(obj):
            msg = "WEB: Object is not in document content."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            super().say_word(obj)
            return

        document = self.utilities.get_top_level_document_for_object(obj)
        obj, offset = self.utilities.get_caret_context(document=document)
        if input_event_manager.get_manager().last_event_was_right():
            offset -= 1

        tokens = ["WEB: Adjusted object and offset for say word to", obj, offset]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        word_contents = self.utilities.get_word_contents_at_offset(obj, offset, use_cache=True)
        text_obj, start_offset, _end_offset, _word = word_contents[0]

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

        speech_presenter.get_presenter().speak_word(self, obj, offset)
        AXUtilities.set_last_text_unit_spoken(TextUnit.WORD)

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

        tokens = ["WEB: Say line for", obj]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        if not self.utilities.in_document_content(obj):
            msg = "WEB: Object is not in document content."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            super().say_line(obj)
            return

        document = self.utilities.get_top_level_document_for_object(obj)
        if offset is None:
            obj, offset = self.utilities.get_caret_context(document)
            tokens = ["WEB: Adjusted object and offset for say line to", obj, offset]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        contents = self.utilities.get_line_contents_at_offset(obj, offset, use_cache=True)
        if (
            contents
            and contents[0]
            and not document_presenter.get_presenter().in_focus_mode(self.app)
        ):
            self.utilities.set_caret_position(contents[0][0], contents[0][1])

        line, start_offset = AXText.get_line_at_offset(obj, offset)[0:2]
        speech_presenter.get_presenter().speak_line(
            self,
            obj,
            start_offset,
            start_offset + len(line),
            line,
        )

        AXUtilities.set_last_text_unit_spoken(TextUnit.LINE)

    def present_object(self, obj: Atspi.Accessible, **args) -> None:
        if obj is None:
            return

        if not self.utilities.in_document_content(obj) or AXUtilities.is_document(obj):
            super().present_object(obj, **args)
            return

        mode, _obj = focus_manager.get_manager().get_active_mode_and_object_of_interest()
        if mode in [focus_manager.OBJECT_NAVIGATOR, focus_manager.MOUSE_REVIEW]:
            super().present_object(obj, **args)
            return

        if AXUtilities.is_status_bar(obj) or AXUtilities.is_alert(obj):
            if not document_presenter.get_presenter().in_focus_mode(self.app):
                self.utilities.set_caret_position(obj, 0)
            super().present_object(obj, **args)
            return

        prior_obj = args.get("priorObj")
        if (
            caret_navigator.get_navigator().last_input_event_was_navigation_command()
            or structural_navigator.get_navigator().last_input_event_was_navigation_command()
            or table_navigator.get_navigator().last_input_event_was_navigation_command()
            or args.get("includeContext")
            or AXUtilities.get_table(obj)
        ):
            prior_context = self.utilities.get_prior_context()
            if prior_context is not None:
                prior_obj, _prior_offset = prior_context
                args["priorObj"] = prior_obj

        # Objects might be destroyed as a consequence of scrolling, such as in an infinite scroll
        # list. Therefore, store its name and role beforehand. Objects in the process of being
        # destroyed typically lose their name even if they lack the defunct state. If the name of
        # the object is different after scrolling, we'll try to find a child with the same name and
        # role.
        document = self.utilities.get_document_for_object(obj)
        name = AXObject.get_name(obj)
        role = AXObject.get_role(obj)
        AXEventSynthesizer.scroll_to_center(obj, start_offset=0)
        if (name and AXObject.get_name(obj) != name) or AXObject.get_index_in_parent(obj) < 0:
            tokens = ["WEB:", obj, "believed to be destroyed after scroll."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            replicant = AXUtilities.find_descendant(
                document,
                lambda x: AXObject.get_name(x) == name and AXObject.get_role(obj) == role,
            )
            if replicant:
                obj = replicant
                tokens = ["WEB: Replacing destroyed object with", obj]
                debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        # Editors like VSCode use the entry role for the code editor.
        if AXUtilities.is_entry(obj):
            if not document_presenter.get_presenter().in_focus_mode(self.app):
                self.utilities.set_caret_position(obj, 0)
            super().present_object(obj, **args)
            return

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

        # We shouldn't use cache in this method, because if the last thing we presented
        # included this object and offset (e.g. a Say All or Mouse Review), we're in
        # danger of presented irrelevant context.
        offset = args.get("offset", 0)
        contents = self.utilities.get_object_contents_at_offset(obj, offset, use_cache=False)
        if (
            contents
            and contents[0]
            and not document_presenter.get_presenter().in_focus_mode(self.app)
        ):
            self.utilities.set_caret_position(contents[0][0], contents[0][1])
        presenter = presentation_manager.get_manager()
        presenter.display_contents(contents)
        presenter.speak_contents(contents, **args)

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

        if "\ufffc" in AXText.get_all_text(obj):
            self.update_braille(obj)
            return

        super()._update_braille_caret_position(obj)

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

        tokens = ["WEB: updating braille for", obj, args]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True, True)

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

        if document_presenter.get_presenter().in_focus_mode(
            self.app,
        ) and "\ufffc" not in AXText.get_all_text(obj):
            tokens = ["WEB: updating braille in focus mode", obj]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            super().update_braille(obj, **args)
            return

        document = args.get("documentFrame", self.utilities.get_top_level_document_for_object(obj))
        if not document:
            tokens = ["WEB: updating braille for non-document object", obj]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            super().update_braille(obj, **args)
            return

        is_content_editable = self.utilities.is_content_editable_with_embedded_objects(obj)

        if (
            not caret_navigator.get_navigator().last_input_event_was_navigation_command()
            and not structural_navigator.get_navigator().last_input_event_was_navigation_command()
            and not table_navigator.get_navigator().last_input_event_was_navigation_command()
            and not is_content_editable
            and not AXDocument.is_plain_text(document)
            and not input_event_manager.get_manager().last_event_was_caret_selection()
        ):
            tokens = ["WEB: updating braille for unhandled navigation type", obj]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            super().update_braille(obj, **args)
            return

        # TODO - JD: Getting the caret context can, by side effect, update it. This in turn
        # can prevent us from presenting table column headers when braille is enabled because
        # we think they are not "new." Commit bd877203f0 addressed that, but we need to stop
        # such side effects from happening in the first place.
        offset = args.get("offset")
        if offset is None:
            obj, offset = self.utilities.get_caret_context(document, get_replicant=True)
        if offset > 0 and is_content_editable and self.utilities.treat_as_text_object(obj):
            offset = min(offset, AXText.get_character_count(obj))

        contents = self.utilities.get_line_contents_at_offset(obj, offset)
        presentation_manager.get_manager().display_contents(contents, documentFrame=document)

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

        if (
            flat_review_presenter.get_presenter().is_active()
            or not self.utilities.in_document_content()
        ):
            return super()._pan_braille_left(event)

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

        # At edge, get previous line from document.
        contents = self.utilities.get_previous_line_contents()
        if not contents:
            return False

        obj, start, _end, _string = contents[0]
        self.utilities.set_caret_position(obj, start)
        self.update_braille(obj)
        presenter.pan_to_end()
        return True

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

        if (
            flat_review_presenter.get_presenter().is_active()
            or not self.utilities.in_document_content()
        ):
            return super()._pan_braille_right(event)

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

        # At edge, get next line from document.
        contents = self.utilities.get_next_line_contents()
        if not contents:
            return False

        obj, start, _end, _string = contents[0]
        self.utilities.set_caret_position(obj, start)
        self.update_braille(obj)
        presenter.pan_to_beginning()
        return True

    def locus_of_focus_changed(
        self,
        event: Atspi.Event | None,
        old_focus: Atspi.Accessible | None,
        new_focus: Atspi.Accessible | None,
    ) -> bool:
        """Handles changes of focus of interest. Returns True if this script did all needed work."""

        tokens = ["WEB: Focus changing from", old_focus, "to", new_focus]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if new_focus and not AXObject.is_valid(new_focus):
            return True

        if new_focus and AXObject.is_dead(new_focus):
            return True

        document = self.utilities.get_top_level_document_for_object(new_focus)
        if not document and self.utilities.is_document(new_focus):
            document = new_focus

        sn_navigator = structural_navigator.get_navigator()
        last_command_was_struct_nav = sn_navigator.last_input_event_was_navigation_command()

        if not document:
            msg = "WEB: Locus of focus changed to non-document obj"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            document_presenter.get_presenter().reset_find_announcement_state()

            old_document = self.utilities.get_top_level_document_for_object(old_focus)
            if not document and self.utilities.is_document(old_focus):
                old_document = old_focus

            if old_focus and not old_document:
                msg = "WEB: Not refreshing grabs because we weren't in a document before"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return False

            if last_command_was_struct_nav and sn_navigator.get_mode(self) == NavigationMode.GUI:
                msg = "WEB: Not refreshing grabs: Last command was GUI structural navigation"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return False

            reason = "locus of focus no longer in document"
            document_presenter.get_presenter().suspend_navigators(self, True, reason)
            return False

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

        caret_offset = 0
        if self.utilities.in_find_container(old_focus) or (
            self.utilities.is_document(new_focus)
            and old_focus == focus_manager.get_manager().get_active_window()
        ):
            context_obj, context_offset = self.utilities.get_caret_context(document)
            if context_obj and AXObject.is_valid(context_obj):
                new_focus, caret_offset = context_obj, context_offset

        if AXUtilities.is_unknown_or_redundant(new_focus):
            msg = "WEB: Event source has bogus role. Likely browser bug."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            new_focus, _offset = self.utilities.first_context(new_focus, 0)

        if self.utilities.treat_as_text_object(new_focus):
            text_offset = AXText.get_caret_offset(new_focus)
            if 0 <= text_offset <= AXText.get_character_count(new_focus):
                caret_offset = text_offset

        self.utilities.set_caret_context(new_focus, caret_offset, document)
        self.update_braille(new_focus, documentFrame=document)

        contents = None
        args = {}
        last_command_was_caret_nav = (
            caret_navigator.get_navigator().last_input_event_was_navigation_command()
        )
        last_command_was_struct_nav = (
            last_command_was_struct_nav
            or table_navigator.get_navigator().last_input_event_was_navigation_command()
        )
        manager = input_event_manager.get_manager()
        last_command_was_line_nav = (
            manager.last_event_was_line_navigation() and not last_command_was_caret_nav
        )

        args["priorObj"] = old_focus
        if (
            manager.last_event_was_mouse_button()
            and event
            and event.type.startswith("object:text-caret-moved")
        ):
            msg = "WEB: Last input event was mouse button. Generating line."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset)
        elif (
            self.utilities.is_content_editable_with_embedded_objects(new_focus)
            and (
                last_command_was_caret_nav
                or last_command_was_struct_nav
                or last_command_was_line_nav
            )
            and not (AXUtilities.is_table_cell(new_focus) and AXObject.get_name(new_focus))
        ):
            tokens = ["WEB: New focus", new_focus, "content editable. Generating line."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset)
        elif AXUtilities.is_anchor(new_focus):
            tokens = ["WEB: New focus", new_focus, "is anchor. Generating line."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            contents = self.utilities.get_line_contents_at_offset(new_focus, 0)
        elif (
            input_event_manager.get_manager().last_event_was_page_navigation()
            and not AXUtilities.get_table(new_focus)
            and not AXUtilities.is_feed_article(new_focus)
        ):
            tokens = ["WEB: New focus", new_focus, "was scrolled to. Generating line."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset)
        elif self.utilities.is_focused_with_math_child(new_focus):
            tokens = ["WEB: New focus", new_focus, "has math child. Generating line."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset)
        elif AXUtilities.is_heading(new_focus):
            tokens = ["WEB: New focus", new_focus, "is heading. Generating object."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            contents = self.utilities.get_object_contents_at_offset(new_focus, 0)
        elif self.utilities.caret_moved_to_same_page_fragment(event, old_focus):
            assert event is not None
            tokens = ["WEB: Source", event.source, "is same page fragment. Generating line."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            contents = self.utilities.get_line_contents_at_offset(new_focus, 0)
        elif (
            event
            and event.type.startswith("object:children-changed:remove")
            and document_presenter.get_presenter().is_focus_mode_widget(self, new_focus)
        ):
            tokens = [
                "WEB: New focus",
                new_focus,
                "is recovery from removed child. Generating speech.",
            ]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        elif last_command_was_line_nav and not AXObject.is_valid(old_focus):
            msg = "WEB: Last input event was line nav; old_focus is invalid. Generating line."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset)
        elif (
            last_command_was_line_nav and event and event.type.startswith("object:children-changed")
        ):
            msg = "WEB: Last input event was line nav and children changed. Generating line."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            contents = self.utilities.get_line_contents_at_offset(new_focus, caret_offset)
        else:
            tokens = ["WEB: New focus", new_focus, "is not a special case. Generating speech."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if new_focus and AXObject.is_dead(new_focus):
            msg = "WEB: New focus has since died"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            if self._get_queued_event("object:state-changed:focused", True):
                msg = "WEB: Have matching focused event. Not speaking contents"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

        presentation_manager.get_manager().interrupt_if_needed_for_focus_change(
            old_focus, new_focus, event
        )

        if contents:
            presentation_manager.get_manager().speak_contents(contents, **args)
        else:
            presentation_manager.get_manager().present_object(
                self,
                new_focus,
                generate_braille=False,
                **args,  # type: ignore[arg-type]
            )

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

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

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if not event.detail1:
            msg = "WEB: Ignoring because event source is now inactive"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.is_dialog_or_alert(event.source):
            msg = "WEB: Event handled: Setting locusOfFocus to event source"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            focus_manager.get_manager().set_locus_of_focus(event, event.source)
            return True

        return False

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

        if AXUtilities.has_no_size(event.source):
            msg = "WEB: Ignoring event from page with no size."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not AXDocument.get_uri(event.source):
            msg = "WEB: Ignoring event from page with no URI."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        AXUtilities.clear_all_cache_now(event.source, "busy-changed event.")

        if event.detail1 and self._loading_content:
            msg = "WEB: Ignoring: Already loading document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        active_document = self.utilities.active_document()
        if active_document and active_document != event.source:
            msg = "WEB: Ignoring: Event source is not active document"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        focus = focus_manager.get_manager().get_locus_of_focus()
        if not AXUtilities.is_document_web(event.source) and not AXUtilities.is_ancestor(
            focus,
            event.source,
            True,
        ):
            msg = "WEB: Ignoring: Not document and not something we're in"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.get_document_for_object(AXObject.get_parent(event.source)):
            msg = "WEB: Ignoring: Event source is nested document"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        obj, offset = self.utilities.get_caret_context()
        if not AXObject.is_valid(obj):
            self.utilities.clear_caret_context()

        should_present = True
        mgr = speech_presenter.get_presenter()
        if mgr.get_only_speak_displayed_text():
            should_present = False
            msg = "WEB: Not presenting due to settings"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        elif not (AXUtilities.is_showing(event.source) or AXUtilities.is_visible(event.source)):
            should_present = False
            msg = "WEB: Not presenting because source is not showing or visible"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        elif not AXDocument.get_uri(event.source):
            should_present = False
            msg = "WEB: Not presenting because source lacks URI"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        elif (
            not event.detail1
            and document_presenter.get_presenter().in_focus_mode(self.app)
            and AXObject.is_valid(obj)
        ):
            should_present = False
            tokens = ["WEB: Not presenting due to focus mode for", obj]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        elif not mgr.use_verbose_speech():
            should_present = not event.detail1
            tokens = ["WEB: Brief verbosity set. Should present", obj, f": {should_present}"]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if should_present and AXDocument.get_uri(event.source).startswith("http"):
            if event.detail1:
                presentation_manager.get_manager().present_message(messages.PAGE_LOADING_START)
            elif AXObject.get_name(event.source):
                if not mgr.use_verbose_speech():
                    msg = AXObject.get_name(event.source)
                else:
                    msg = messages.PAGE_LOADING_END_NAMED % AXObject.get_name(event.source)
                presentation_manager.get_manager().present_message(msg)
            else:
                presentation_manager.get_manager().present_message(messages.PAGE_LOADING_END)

        self._loading_content = event.detail1
        if event.detail1:
            return True

        self.utilities.clear_cached_objects()
        if AXObject.is_dead(obj):
            obj = None

        if (
            not focus_manager.get_manager().focus_is_dead()
            and not self.utilities.in_document_content(focus)
            and AXUtilities.is_focused(focus)
        ):
            msg = "WEB: Not presenting content, focus is outside of document"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if document_presenter.get_presenter().get_page_summary_on_load() and should_present:
            tokens = ["WEB: Getting page summary for", event.source]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            summary = AXDocument.get_document_summary(event.source)
            if summary:
                presentation_manager.get_manager().present_message(summary)

        obj, offset = self.utilities.get_caret_context()
        if not AXUtilities.is_busy(event.source):
            document_presenter.get_presenter().update_mode_if_needed(self, focus, obj)

        if not obj:
            msg = "WEB: Could not get caret context"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if document_presenter.get_presenter().is_focus_mode_widget(self, obj):
            tokens = ["WEB: Setting locus of focus to focusModeWidget", obj]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            focus_manager.get_manager().set_locus_of_focus(event, obj)
            return True

        if self.utilities.is_link(obj) and AXUtilities.is_focused(obj):
            tokens = ["WEB: Setting locus of focus to focused link", obj, ". No SayAll."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            focus_manager.get_manager().set_locus_of_focus(event, obj)
            return True

        if offset > 0:
            tokens = ["WEB: Setting locus of focus to context obj", obj, ". No SayAll"]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            focus_manager.get_manager().set_locus_of_focus(event, obj)
            return True

        if not AXUtilities.is_focused(focus_manager.get_manager().get_locus_of_focus()):
            tokens = ["WEB: Setting locus of focus to context obj", obj, "(no notification)"]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            focus_manager.get_manager().set_locus_of_focus(event, obj, False)

        self.update_braille(obj)
        if AXDocument.get_document_uri_fragment(event.source):
            msg = "WEB: Not doing SayAll due to page fragment"
            debug.print_message(debug.LEVEL_INFO, msg, True)
        elif not document_presenter.get_presenter().get_say_all_on_load():
            msg = "WEB: Not doing SayAll due to sayAllOnLoad being False"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            presentation_manager.get_manager().speak_contents(
                self.utilities.get_line_contents_at_offset(obj, offset),
            )
        elif speech_manager.get_manager().get_speech_is_enabled_and_not_muted():
            msg = "WEB: Doing SayAll"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            say_all_presenter.get_presenter().say_all(self, None)
        else:
            msg = "WEB: Not doing SayAll due to speech being disabled or muted"
            debug.print_message(debug.LEVEL_INFO, msg, True)

        return True

    def _on_caret_moved(self, event: Atspi.Event) -> bool:
        """Callback for object:text-caret-moved accessibility events."""

        reason = AXUtilities.get_text_event_reason(event)
        document = self.utilities.get_top_level_document_for_object(event.source)
        if not document:
            if self.utilities.event_is_browser_ui_noise_deprecated(event):
                msg = "WEB: Ignoring event believed to be browser UI noise"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            if self.utilities.event_is_browser_ui_autocomplete_noise_deprecated(event):
                msg = "WEB: Ignoring event believed to be browser UI autocomplete noise"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            if self.utilities.in_find_container() and reason not in (
                TextEventReason.NAVIGATION_BY_CHARACTER,
                TextEventReason.NAVIGATION_BY_WORD,
                TextEventReason.NAVIGATION_TO_LINE_BOUNDARY,
            ):
                msg = "WEB: Event handled: Presenting find results"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                document_presenter.get_presenter().present_find_results(event.source, event.detail1)
                return True

            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        obj, offset = self.utilities.get_caret_context(document, False, False)
        tokens = ["WEB: Context: ", obj, ", ", offset]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        # TODO - JD: Make this a TextEventReason.
        if caret_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "WEB: Event ignored: Last command was caret nav"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        # TODO - JD: Make this a TextEventReason.
        if structural_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "WEB: Event ignored: Last command was struct nav"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        # TODO - JD: Make this a TextEventReason.
        if table_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "WEB: Event ignored: Last command was table nav"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if reason == TextEventReason.SAY_ALL:
            msg = "WEB: Event handled: SayAll triggered"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if reason == TextEventReason.MOUSE_PRIMARY_BUTTON:
            if (event.source, event.detail1) == (obj, offset):
                msg = "WEB: Event is for current caret context."
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            if (event.source, event.detail1) == self._last_mouse_button_context:
                msg = "WEB: Event is for last mouse button context."
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            self._last_mouse_button_context = event.source, event.detail1

            msg = "WEB: Event handled: Last command was mouse button"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self.utilities.set_caret_context(event.source, event.detail1)
            notify = AXUtilities.find_ancestor_inclusive(obj, AXUtilities.is_entry) is None
            focus_manager.get_manager().set_locus_of_focus(event, event.source, notify, True)
            return True

        if reason == TextEventReason.FOCUS_CHANGE:
            msg = "WEB: Event ignored: Caret moved due to focus change."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not self.utilities.event_is_from_locus_of_focus_document(event):
            msg = "WEB: Event ignored: Not from locus of focus document"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if reason == TextEventReason.UI_UPDATE:
            msg = "WEB: Event ignored: Caret moved due to UI update."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if reason in [TextEventReason.TYPING, TextEventReason.TYPING_ECHOABLE]:
            msg = "WEB: Event handled: Updating position due to insertion"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            focus_manager.get_manager().set_last_cursor_position(event.source, event.detail1)
            return True

        if reason in (TextEventReason.DELETE, TextEventReason.BACKSPACE):
            msg = "WEB: Event handled: Updating position due to deletion"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            focus_manager.get_manager().set_last_cursor_position(event.source, event.detail1)
            return True

        focus = focus_manager.get_manager().get_locus_of_focus()
        i_e_manager = input_event_manager.get_manager()
        if (
            self.utilities.is_item_for_editable_combo_box(focus, event.source)
            and not i_e_manager.last_event_was_character_navigation()
            and not i_e_manager.last_event_was_line_boundary_navigation()
        ):
            msg = "WEB: Event ignored: Editable combobox noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.event_is_autocomplete_noise_deprecated(event, document):
            msg = "WEB: Event ignored: Autocomplete noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if document_presenter.get_presenter().in_focus_mode(
            self.app,
        ) and self.utilities.caret_moved_outside_active_grid(event):
            msg = "WEB: Event ignored: Caret moved outside active grid during focus mode"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.treat_event_as_spinner_value_change_deprecated(event):
            msg = "WEB: Event handled as the value-change event we wish we'd get"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self.update_braille(event.source)
            self._present_caret_moved_event(event)
            return True

        if not self.utilities.treat_as_text_object(event.source) and not AXUtilities.is_editable(
            event.source,
        ):
            msg = "WEB: Event ignored: Was for non-editable object we're treating as textless"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        notify = force = handled = False
        AXObject.clear_cache(event.source, False, "Updating state for caret moved event.")

        if document_presenter.get_presenter().in_focus_mode(self.app):
            obj, offset = event.source, event.detail1
        else:
            obj, offset = self.utilities.first_context(event.source, event.detail1)

        if reason == TextEventReason.NAVIGATION_BY_PAGE:
            msg = "WEB: Caret moved due to scrolling."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            notify = force = handled = True

        elif self.utilities.caret_moved_to_same_page_fragment(event):
            msg = "WEB: Caret moved to fragment."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            notify = force = handled = True

        elif (
            event.source != focus
            and AXUtilities.is_editable(event.source)
            and (AXUtilities.is_focused(event.source) or not AXUtilities.is_focusable(event.source))
        ):
            msg = "WEB: Editable object is not (yet) the locus of focus."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            notify = force = handled = (
                i_e_manager.last_event_was_line_navigation()
                or i_e_manager.last_event_was_return()
                or i_e_manager.last_event_was_command()
            )

        elif i_e_manager.last_event_was_caret_navigation():
            msg = "WEB: Caret moved due to native caret navigation."
            debug.print_message(debug.LEVEL_INFO, msg, True)

        tokens = ["WEB: Setting context and focus to: ", obj, ", ", offset, f", notify: {notify}"]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        self.utilities.set_caret_context(obj, offset, document)
        focus_manager.get_manager().set_locus_of_focus(event, obj, notify, force)
        return handled

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

        msg = "WEB: This event is is handled by the toolkit or default script."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return False

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

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

        if self.utilities.event_is_browser_ui_noise_deprecated(event):
            msg = "WEB: Ignoring event believed to be browser UI noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        is_live_region = AXUtilities.is_live_region(event.source)
        document = self.utilities.get_top_level_document_for_object(event.source)
        if document and not is_live_region:
            focus = focus_manager.get_manager().get_locus_of_focus()
            if event.source == focus:
                msg = "WEB: Dumping cache: source is focus"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self.utilities.dump_cache(document, preserve_context=True)
            elif focus_manager.get_manager().focus_is_dead():
                msg = "WEB: Dumping cache: dead focus"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self.utilities.dump_cache(document, preserve_context=True)
            elif AXUtilities.find_ancestor(focus, lambda x: x == event.source):
                msg = "WEB: Dumping cache: source is ancestor of focus"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self.utilities.dump_cache(document, preserve_context=True)
            else:
                msg = "WEB: Not dumping full cache"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self.utilities.clear_cached_objects()
        elif is_live_region:
            msg = "WEB: Ignoring event from live region."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True
        else:
            msg = "WEB: Could not get document for event source"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if self._loading_content:
            msg = "WEB: Ignoring because document content is being loaded."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not AXObject.is_valid(document):
            tokens = ["WEB: Ignoring because", document, "is not valid."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return True

        if AXUtilities.is_busy(document):
            tokens = ["WEB: Ignoring because", document, "is busy."]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return True

        if self.utilities.handle_event_from_context_replicant(event, event.any_data):
            msg = "WEB: Event handled by updating locusOfFocus and context to child."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.is_alert(event.any_data):
            msg = "WEB: Presenting event.any_data"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self.present_object(event.any_data, interrupt=True)

            focused = AXUtilities.get_focused_object(event.any_data)
            if focused:
                notify = not self.utilities.treat_as_text_object(focused)
                tokens = ["WEB: Setting locusOfFocus and caret context to", focused]
                debug.print_tokens(debug.LEVEL_INFO, tokens, True)
                focus_manager.get_manager().set_locus_of_focus(event, focused, notify)
                self.utilities.set_caret_context(focused, 0)
            return True

        return False

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

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

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if self._loading_content:
            msg = "WEB: Ignoring because document content is being loaded."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.is_live_region(event.source):
            if self.utilities.handle_event_for_removed_child(event):
                msg = "WEB: Event handled for removed live-region child."
                debug.print_message(debug.LEVEL_INFO, msg, True)
            else:
                msg = "WEB: Ignoring removal from live region."
                debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        document = self.utilities.get_top_level_document_for_object(event.source)
        if document:
            focus = focus_manager.get_manager().get_locus_of_focus()
            if event.source == focus:
                msg = "WEB: Dumping cache: source is focus"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self.utilities.dump_cache(document, preserve_context=True)
            elif focus_manager.get_manager().focus_is_dead():
                msg = "WEB: Dumping cache: dead focus"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self.utilities.dump_cache(document, preserve_context=True)
            elif AXUtilities.find_ancestor(focus, lambda x: x == event.source):
                msg = "WEB: Dumping cache: source is ancestor of focus"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self.utilities.dump_cache(document, preserve_context=True)
            else:
                msg = "WEB: Not dumping full cache"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                self.utilities.clear_cached_objects()

        if self.utilities.handle_event_for_removed_child(event):
            msg = "WEB: Event handled for removed child."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

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

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        focus = focus_manager.get_manager().get_locus_of_focus()
        if event.source != AXUtilities.get_table(focus):
            msg = "WEB: focus is not in this table"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        presentation_manager.get_manager().present_message(messages.TABLE_REORDERED_COLUMNS)
        header = AXUtilities.find_ancestor_inclusive(focus, AXUtilities.is_table_header)
        msg = AXUtilities.get_presentable_sort_order_from_header(header, True)
        if msg:
            presentation_manager.get_manager().present_message(msg)
        return True

    def _on_document_load_complete(self, event: Atspi.Event) -> bool:
        """Callback for document:load-complete accessibility events."""

        if AXUtilities.has_no_size(event.source):
            msg = "WEB: Ignoring event from page with no size."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        uri = AXDocument.get_uri(event.source)
        if not uri:
            msg = "WEB: Ignoring event from page with no URI."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if uri.startswith("moz-extension"):
            msg = f"WEB: Ignoring event from page with URI: {uri}"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        AXUtilities.clear_all_cache_now(event.source, "load-complete event.")
        if self.utilities.get_document_for_object(AXObject.get_parent(event.source)):
            msg = "WEB: Ignoring: Event source is nested document"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        msg = "WEB: Updating loading state and resetting live regions"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._loading_content = False
        live_region_presenter.get_presenter().reset()
        return True

    def _on_document_load_stopped(self, event: Atspi.Event) -> bool:
        """Callback for document:load-stopped accessibility events."""

        if not AXDocument.get_uri(event.source):
            msg = "WEB: Ignoring event from page with no URI."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.get_document_for_object(AXObject.get_parent(event.source)):
            msg = "WEB: Ignoring: Event source is nested document"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        msg = "WEB: Updating loading state"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._loading_content = False
        return True

    def _on_document_reload(self, event: Atspi.Event) -> bool:
        """Callback for document:reload accessibility events."""

        if not AXDocument.get_uri(event.source):
            msg = "WEB: Ignoring event from page with no URI."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.get_document_for_object(AXObject.get_parent(event.source)):
            msg = "WEB: Ignoring: Event source is nested document"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        msg = "WEB: Updating loading state"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._loading_content = True
        return True

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

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        focus = focus_manager.get_manager().get_locus_of_focus()
        obj, offset = self.utilities.get_caret_context(search_if_needed=False)
        tokens = ["WEB: Caret context is", obj, ", ", offset]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        if not AXObject.is_valid(obj) and event.source == focus:
            msg = "WEB: Setting caret context to event source"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self.utilities.set_caret_context(event.source, 0)

        return False

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

        if not event.detail1:
            msg = "WEB: Ignoring because event source lost focus"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        document = self.utilities.get_top_level_document_for_object(event.source)
        if not document:
            msg = "WEB: Could not get document for event source"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if focus_manager.get_manager().in_say_all():
            msg = "WEB: Ignoring focus change during say all"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        focus = focus_manager.get_manager().get_locus_of_focus()
        prev_document = self.utilities.get_top_level_document_for_object(focus)
        if prev_document != document:
            tokens = ["WEB: document changed from", prev_document, "to", document]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        elif document == event.source:
            msg = "WEB: Ignoring focus change to document ancestor of focus"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.is_link(event.source) and AXUtilities.is_ancestor(focus, event.source):
            msg = "WEB: Ignoring focus change on link ancestor of focus"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if caret_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "WEB: Event ignored: Last command was caret nav"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if structural_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "WEB: Event ignored: Last command was struct nav"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if table_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "WEB: Event ignored: Last command was table nav"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if (
            document_presenter.get_presenter().browse_mode_is_sticky(self.app)
            and not input_event_manager.get_manager().last_event_was_tab_navigation()
        ):
            msg = "WEB: Element claimed focus, but browse mode is sticky"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.find_ancestor(event.source, AXUtilities.is_embedded):
            if AXUtilities.is_tool_tip(event.source) and AXUtilities.is_ancestor(
                focus, event.source
            ):
                msg = "WEB: Event believed to be side effect of tooltip navigation."
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            msg = "WEB: Event handled: Setting locusOfFocus to embedded descendant"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            presentation_manager.get_manager().interrupt_if_needed_for_focus_change(
                focus, event.source, event
            )

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

        if AXUtilities.is_editable(event.source):
            msg = "WEB: Event source is editable"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if AXUtilities.is_dialog_or_alert(event.source):
            if AXUtilities.is_ancestor(focus, event.source, True):
                msg = "WEB: Ignoring event from ancestor of focus"
                debug.print_message(debug.LEVEL_INFO, msg, True)
            else:
                msg = "WEB: Event handled: Setting locusOfFocus to event source"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                focus_manager.get_manager().set_locus_of_focus(event, event.source)
            return True

        if self.utilities.handle_event_from_context_replicant(event, event.source):
            msg = "WEB: Event handled by updating locusOfFocus and context to source."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        obj, offset = self.utilities.get_caret_context()
        tokens = ["WEB: Caret context is", obj, ", ", offset]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)

        if not AXObject.is_valid(obj) or prev_document != document:
            tokens = ["WEB: Clearing context - obj", obj, "is not valid or document changed"]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            self.utilities.clear_caret_context()

            obj, offset = self.utilities.search_for_caret_context(event.source)
            if obj:
                tokens = ["WEB: Updating focus and context to", obj, ", ", offset]
                debug.print_tokens(debug.LEVEL_INFO, tokens, True)
                focus_manager.get_manager().set_locus_of_focus(event, obj)
                self.utilities.set_caret_context(obj, offset)
            else:
                msg = "WEB: Search for caret context failed"
                debug.print_message(debug.LEVEL_INFO, msg, True)

        if not (AXUtilities.is_focusable(event.source) and AXUtilities.is_focused(event.source)):
            msg = "WEB: Event ignored: Source is not focusable or focused"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not AXUtilities.is_document(event.source):
            msg = "WEB: Deferring to other scripts for handling non-document source"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if not obj:
            msg = "WEB: Unable to get valid context object"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if input_event_manager.get_manager().last_event_was_page_navigation():
            msg = "WEB: Event handled: Focus changed due to scrolling"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            focus_manager.get_manager().set_locus_of_focus(event, obj)
            self.utilities.set_caret_context(obj, offset)
            return True

        # TODO - JD: Can this logic be removed?
        was_focused = AXUtilities.is_focused(obj)
        AXObject.clear_cache(obj, False, "Sanity-checking focused state.")
        is_focused = AXUtilities.is_focused(obj)
        if was_focused != is_focused:
            tokens = ["WEB: Focused state of", obj, "changed to", is_focused]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return False

        if AXUtilities.is_anchor(obj):
            cause = "Context is anchor"
        elif not (self.utilities.is_link(obj) and not is_focused):
            cause = "Context is not a non-focused link"
        elif self.utilities.is_child_of_current_fragment(obj):
            cause = "Context is child of current fragment"
        elif document == event.source and AXDocument.get_document_uri_fragment(event.source):
            cause = "Document URI is fragment"
        else:
            return False

        tokens = ["WEB: Event handled: Setting locusOfFocus to", obj, "(", cause, ")"]
        debug.print_tokens(debug.LEVEL_INFO, tokens, True)
        focus_manager.get_manager().set_locus_of_focus(event, obj)
        return True

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

        return False

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

        if self.utilities.event_is_browser_ui_noise_deprecated(event):
            msg = "WEB: Ignoring event believed to be browser UI noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

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

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        focus = focus_manager.get_manager().get_locus_of_focus()
        if event.source != AXUtilities.get_table(focus):
            msg = "WEB: focus is not in this table"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        presentation_manager.get_manager().present_message(messages.TABLE_REORDERED_ROWS)
        header = AXUtilities.find_ancestor_inclusive(focus, AXUtilities.is_table_header)
        msg = AXUtilities.get_presentable_sort_order_from_header(header, True)
        if msg:
            presentation_manager.get_manager().present_message(msg)
        return True

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

        if self.utilities.event_is_browser_ui_autocomplete_noise_deprecated(event):
            msg = "WEB: Ignoring event believed to be browser UI autocomplete noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        focus = focus_manager.get_manager().get_locus_of_focus()
        if self.utilities.event_is_browser_ui_page_switch(event):
            msg = "WEB: Event believed to be browser UI page switch"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            if event.detail1:
                # https://bugzilla.mozilla.org/show_bug.cgi?id=1867044
                AXObject.clear_cache(event.source, False, "Work around Gecko bug.")
                AXUtilities.clear_all_cache_now(reason=msg)
                self.present_object(event.source, priorObj=focus, interrupt=True)
            return True

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if focus != event.source:
            msg = "WEB: Ignoring because event source is not locusOfFocus"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

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

        if self.utilities.event_is_browser_ui_autocomplete_noise_deprecated(event):
            msg = "WEB: Ignoring event believed to be browser UI autocomplete noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.event_is_browser_ui_page_switch(event):
            msg = "WEB: Ignoring event believed to be browser UI page switch"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if not self.utilities.in_document_content(focus_manager.get_manager().get_locus_of_focus()):
            msg = "WEB: Event ignored: locusOfFocus is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not self.utilities.event_is_from_locus_of_focus_document(event):
            msg = "WEB: Event ignored: Not from locus of focus document"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if AXUtilities.find_ancestor(event.source, AXUtilities.is_embedded):
            if document_presenter.get_presenter().in_focus_mode(self.app):
                # Because we cannot count on the app firing the right state-changed events
                # for descendants.
                AXObject.clear_cache(
                    event.source,
                    True,
                    "Workaround for missing events on descendants.",
                )
                msg = "WEB: Event source is embedded descendant and we're in focus mode"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return False

            msg = "WEB: Event source is embedded descendant and we're in browse mode"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.event_is_irrelevant_selection_changed_event(event):
            msg = "WEB: Event ignored: Irrelevant"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        obj, _offset = self.utilities.get_caret_context()
        ancestor = AXUtilities.get_common_ancestor(obj, event.source)
        if ancestor and self.utilities.is_text_block_element(ancestor):
            msg = "WEB: Ignoring: Common ancestor of context and event source is text block"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

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

        if event.detail1 and self.utilities.is_browser_ui_alert(event.source):
            msg = "WEB: Event handled: Presenting event source"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self.present_object(event.source, interrupt=True)
            return True

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        return True

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

        msg = "WEB: This event is is handled by the toolkit or default script."
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return False

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

        reason = AXUtilities.get_text_event_reason(event)
        if reason == TextEventReason.PAGE_SWITCH:
            msg = "WEB: Deletion is believed to be due to page switch"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if reason == TextEventReason.LIVE_REGION_UPDATE:
            msg = "WEB: Ignoring deletion from live region"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)

            if reason == TextEventReason.UI_UPDATE:
                msg = "WEB: Ignoring event believed to be browser UI update"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            return False

        if reason == TextEventReason.SPIN_BUTTON_VALUE_CHANGE:
            msg = "WEB: Ignoring: Event believed to be spin button value change"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if reason == TextEventReason.AUTO_DELETION:
            msg = "WEB: Ignoring event believed to be auto deletion"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        msg = "WEB: Clearing content cache due to text deletion"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self.utilities.clear_content_cache()

        if reason in (TextEventReason.DELETE, TextEventReason.BACKSPACE):
            msg = "WEB: Event believed to be due to editable text deletion"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if reason in [TextEventReason.TYPING, TextEventReason.TYPING_ECHOABLE]:
            msg = "WEB: Ignoring event believed to be due to text insertion"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        obj, _offset = self.utilities.get_caret_context(get_replicant=False)
        if (
            obj
            and obj != event.source
            and not AXUtilities.find_ancestor(obj, lambda x: x == event.source)
        ):
            tokens = ["WEB: Ignoring event because it isn't", obj, "or its ancestor"]
            debug.print_tokens(debug.LEVEL_INFO, tokens, True)
            return True

        if not AXObject.is_valid(obj):
            if self.utilities.is_link(obj):
                msg = "WEB: Focused link deleted. Taking no further action."
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            obj, _offset = self.utilities.get_caret_context(get_replicant=True)
            if obj:
                focus_manager.get_manager().set_locus_of_focus(event, obj, notify_script=False)

        if not AXObject.is_valid(obj):
            msg = "WEB: Unable to get valid context object"
            debug.print_message(debug.LEVEL_INFO, msg, True)

        if not AXUtilities.is_editable(
            event.source,
        ) and not self.utilities.is_content_editable_with_embedded_objects(event.source):
            msg = "WEB: Done processing non-editable source"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

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

        reason = AXUtilities.get_text_event_reason(event)
        if reason == TextEventReason.PAGE_SWITCH:
            msg = "WEB: Insertion is believed to be due to page switch"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

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

        if reason == TextEventReason.CHILDREN_CHANGE:
            msg = "WEB: Ignoring: Event believed to be due to children change"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)

            if reason == TextEventReason.UI_UPDATE:
                msg = "WEB: Ignoring event believed to be browser UI update"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            return False

        if reason == TextEventReason.SPIN_BUTTON_VALUE_CHANGE:
            msg = "WEB: Ignoring: Event believed to be spin button value change"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if reason == TextEventReason.AUTO_INSERTION_PRESENTABLE:
            msg = "WEB: Ignoring: Event believed to be auto insertion"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        msg = "WEB: Clearing content cache due to text insertion"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self.utilities.clear_content_cache()

        document = self.utilities.get_top_level_document_for_object(event.source)
        if focus_manager.get_manager().focus_is_dead():
            msg = "WEB: Dumping cache: dead focus"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            self.utilities.dump_cache(document, preserve_context=True)

            if AXUtilities.is_focused(event.source):
                msg = "WEB: Event handled: Setting locusOfFocus to event source"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                focus_manager.get_manager().set_locus_of_focus(None, event.source, force=True)
                return True

        if not self.utilities.treat_as_text_object(event.source):
            msg = "WEB: Ignoring: Event source is not a text object"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        source_is_focus = event.source == focus_manager.get_manager().get_locus_of_focus()
        if not AXUtilities.is_editable(event.source):
            if not source_is_focus:
                msg = "WEB: Done processing non-editable, non-locusOfFocus source"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                return True

            if self.utilities.is_clickable_element(event.source):
                msg = "WEB: Event handled: Re-setting locusOfFocus to changed clickable"
                debug.print_message(debug.LEVEL_INFO, msg, True)
                focus_manager.get_manager().set_locus_of_focus(None, event.source, force=True)
                return True

        if (
            not source_is_focus
            and AXUtilities.is_text_input(event.source)
            and AXUtilities.is_focused(event.source)
        ):
            msg = "WEB: Focused entry is not the locus of focus. Waiting for focus event."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

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

        _reason = AXUtilities.get_text_event_reason(event)
        if self.utilities.event_is_browser_ui_noise_deprecated(event):
            msg = "WEB: Ignoring event believed to be browser UI noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.in_find_container():
            msg = "WEB: Event handled: Presenting find results"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            document_presenter.get_presenter().present_find_results(event.source, 0)
            return True

        if not self.utilities.in_document_content(event.source):
            msg = "WEB: Event source is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        focus = focus_manager.get_manager().get_locus_of_focus()
        if not self.utilities.in_document_content(focus):
            msg = "WEB: Locus of focus is not in document content"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.event_is_autocomplete_noise_deprecated(event):
            msg = "WEB: Ignoring: Event believed to be autocomplete noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.event_is_spinner_noise_deprecated(event):
            msg = "WEB: Ignoring: Event believed to be spinner noise"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.event_is_for_non_navigable_text_object(event):
            msg = "WEB: Ignoring event for non-navigable text object"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if not self.utilities.treat_as_text_object(event.source):
            msg = "WEB: Ignoring: Event source is not a text object"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if (
            event.source != focus
            and AXUtilities.is_text_input(event.source)
            and AXUtilities.is_focused(event.source)
        ):
            msg = "WEB: Focused entry is not the locus of focus. Waiting for focus event."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if self.utilities.is_content_editable_with_embedded_objects(event.source):
            msg = "WEB: In content editable with embedded objects"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return False

        if structural_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "WEB: Ignoring: Last input event was structural navigation command."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        if table_navigator.get_navigator().last_input_event_was_navigation_command():
            msg = "WEB: Ignoring: Last input event was table navigation command."
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        char = AXText.get_character_at_offset(event.source)[0]
        manager = input_event_manager.get_manager()
        if (
            char == "\ufffc"
            and not manager.last_event_was_caret_selection()
            and not manager.last_event_was_command()
        ):
            msg = "WEB: Ignoring: Not selecting and event offset is at embedded object"
            debug.print_message(debug.LEVEL_INFO, msg, True)
            return True

        return False

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

        msg = "WEB: Deferring to app/toolkit script"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        return False

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

        msg = "WEB: Clearing command state"
        debug.print_message(debug.LEVEL_INFO, msg, True)
        self._last_mouse_button_context = None, -1
        return False
