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

"""The default Script for presenting information to the user using
both speech and Braille.  This is based primarily on the de-facto
standard implementation of the AT-SPI, which is the GAIL support
for GTK."""

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2004-2009 Sun Microsystems Inc." \
                "Copyright (c) 2010 Joanmarie Diggs"
__license__   = "LGPL"

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

import re
import time

import cthulhu.braille as braille
import cthulhu.cmdnames as cmdnames
import cthulhu.debug as debug
import cthulhu.find as find
import cthulhu.flat_review as flat_review
import cthulhu.input_event as input_event
import cthulhu.keybindings as keybindings
import cthulhu.messages as messages
import cthulhu.cthulhu as cthulhu
import cthulhu.cthulhu_state as cthulhu_state
import cthulhu.phonnames as phonnames
import cthulhu.script as script
import cthulhu.script_manager as script_manager
import cthulhu.settings as settings
import cthulhu.settings_manager as settings_manager
import cthulhu.sound as sound
import cthulhu.speech as speech
import cthulhu.speechserver as speechserver
from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities

_scriptManager = script_manager.getManager()
_settingsManager = settings_manager.getManager()

########################################################################
#                                                                      #
# The Default script class.                                            #
#                                                                      #
########################################################################

class Script(script.Script):

    EMBEDDED_OBJECT_CHARACTER = '\ufffc'
    NO_BREAK_SPACE_CHARACTER  = '\u00a0'

    # generatorCache
    #
    DISPLAYED_LABEL = 'displayedLabel'
    DISPLAYED_TEXT = 'displayedText'
    KEY_BINDING = 'keyBinding'
    NESTING_LEVEL = 'nestingLevel'
    NODE_LEVEL = 'nodeLevel'
    REAL_ACTIVE_DESCENDANT = 'realActiveDescendant'

    def __init__(self, app):
        """Creates a new script for the given application.

        Arguments:
        - app: the application to create a script for.
        """
        script.Script.__init__(self, app)

        self.targetCursorCell = None

        self.justEnteredFlatReviewMode = False

        self.digits = '0123456789'
        self.whitespace = ' \t\n\r\v\f'

        # A dictionary of non-standardly-named text attributes and their
        # Atk equivalents.
        #
        self.attributeNamesDict = {}

        # Keep track of the last time we issued a mouse routing command
        # so that we can guess if a change resulted from our moving the
        # pointer.
        #
        self.lastMouseRoutingTime = None

        # The last location of the mouse, which we might want if routing
        # the pointer elsewhere.
        #
        self.oldMouseCoordinates = [0, 0]

        self._lastWordCheckedForSpelling = ""

        self._inSayAll = False
        self._sayAllIsInterrupted = False
        self._sayAllContexts = []
        self.grab_ids = []

        if app:
            Atspi.Accessible.set_cache_mask(
                app, Atspi.Cache.DEFAULT ^ Atspi.Cache.NAME ^ Atspi.Cache.DESCRIPTION)

    def setupInputEventHandlers(self):
        """Defines InputEventHandler fields for this script that can be
        called by the key and braille bindings."""

        self.inputEventHandlers["routePointerToItemHandler"] = \
            input_event.InputEventHandler(
                Script.routePointerToItem,
                cmdnames.ROUTE_POINTER_TO_ITEM)

        self.inputEventHandlers["leftClickReviewItemHandler"] = \
            input_event.InputEventHandler(
                Script.leftClickReviewItem,
                cmdnames.LEFT_CLICK_REVIEW_ITEM)

        self.inputEventHandlers["rightClickReviewItemHandler"] = \
             input_event.InputEventHandler(
                Script.rightClickReviewItem,
                cmdnames.RIGHT_CLICK_REVIEW_ITEM)

        self.inputEventHandlers["sayAllHandler"] = \
            input_event.InputEventHandler(
                Script.sayAll,
                cmdnames.SAY_ALL)

        self.inputEventHandlers["findHandler"] = \
            input_event.InputEventHandler(
                cthulhu.showFindGUI,
                cmdnames.SHOW_FIND_GUI)

        self.inputEventHandlers["findNextHandler"] = \
            input_event.InputEventHandler(
                Script.findNext,
                cmdnames.FIND_NEXT)

        self.inputEventHandlers["findPreviousHandler"] = \
            input_event.InputEventHandler(
                Script.findPrevious,
                cmdnames.FIND_PREVIOUS)

        self.inputEventHandlers["panBrailleLeftHandler"] = \
            input_event.InputEventHandler(
                Script.panBrailleLeft,
                cmdnames.PAN_BRAILLE_LEFT,
                False) # Do not enable learn mode for this action

        self.inputEventHandlers["panBrailleRightHandler"] = \
            input_event.InputEventHandler(
                Script.panBrailleRight,
                cmdnames.PAN_BRAILLE_RIGHT,
                False) # Do not enable learn mode for this action

        self.inputEventHandlers["goBrailleHomeHandler"] = \
            input_event.InputEventHandler(
                Script.goBrailleHome,
                cmdnames.GO_BRAILLE_HOME)

        self.inputEventHandlers["contractedBrailleHandler"] = \
            input_event.InputEventHandler(
                Script.setContractedBraille,
                cmdnames.SET_CONTRACTED_BRAILLE)

        self.inputEventHandlers["processRoutingKeyHandler"] = \
            input_event.InputEventHandler(
                Script.processRoutingKey,
                cmdnames.PROCESS_ROUTING_KEY)

        self.inputEventHandlers["processBrailleCutBeginHandler"] = \
            input_event.InputEventHandler(
                Script.processBrailleCutBegin,
                cmdnames.PROCESS_BRAILLE_CUT_BEGIN)

        self.inputEventHandlers["processBrailleCutLineHandler"] = \
            input_event.InputEventHandler(
                Script.processBrailleCutLine,
                cmdnames.PROCESS_BRAILLE_CUT_LINE)

        self.inputEventHandlers["shutdownHandler"] = \
            input_event.InputEventHandler(
                cthulhu.quitCthulhu,
                cmdnames.QUIT_CTHULHU)

        self.inputEventHandlers["cycleSettingsProfileHandler"] = \
            input_event.InputEventHandler(
                Script.cycleSettingsProfile,
                cmdnames.CYCLE_SETTINGS_PROFILE)

        self.inputEventHandlers["cycleDebugLevelHandler"] = \
            input_event.InputEventHandler(
                Script.cycleDebugLevel,
                cmdnames.CYCLE_DEBUG_LEVEL)

        self.inputEventHandlers["bypassNextCommandHandler"] = \
            input_event.InputEventHandler(
                Script.bypassNextCommand,
                cmdnames.BYPASS_NEXT_COMMAND)

        self.inputEventHandlers.update(self.notificationPresenter.get_handlers())
        self.inputEventHandlers.update(self.flatReviewPresenter.get_handlers())
        self.inputEventHandlers.update(self.speechAndVerbosityManager.get_handlers())
        self.inputEventHandlers.update(self.dateAndTimePresenter.get_handlers())
        self.inputEventHandlers.update(self.bookmarks.get_handlers())
        self.inputEventHandlers.update(self.objectNavigator.get_handlers())
        self.inputEventHandlers.update(self.whereAmIPresenter.get_handlers())
        self.inputEventHandlers.update(self.learnModePresenter.get_handlers())
        self.inputEventHandlers.update(self.mouseReviewer.get_handlers())
        self.inputEventHandlers.update(self.actionPresenter.get_handlers())
        cthulhu.getManager().getDynamicApiManager().registerAPI('inputEventHandlers',self.inputEventHandlers, overwrite=True)
        cthulhu.getManager().getSignalManager().emitSignal('setup-inputeventhandlers-completed')
        
    def getInputEventHandlerKey(self, inputEventHandler):
        """Returns the name of the key that contains an inputEventHadler
        passed as argument
        """

        for keyName, handler in self.inputEventHandlers.items():
            if handler == inputEventHandler:
                return keyName

        return None

    def getListeners(self):
        """Sets up the AT-SPI event listeners for this script.
        """
        listeners = script.Script.getListeners(self)
        listeners["focus:"]                                 = \
            self.onFocus
        #listeners["keyboard:modifiers"]                     = \
        #    self.noOp
        listeners["document:reload"]                        = \
            self.onDocumentReload
        listeners["document:load-complete"]                 = \
            self.onDocumentLoadComplete
        listeners["document:load-stopped"]                  = \
            self.onDocumentLoadStopped
        listeners["mouse:button"]                           = \
            self.onMouseButton
        listeners["object:announcement"]                    = \
            self.onAnnouncement
        listeners["object:property-change:accessible-name"] = \
            self.onNameChanged
        listeners["object:property-change:accessible-description"] = \
            self.onDescriptionChanged
        listeners["object:text-caret-moved"]                = \
            self.onCaretMoved
        listeners["object:text-changed:delete"]             = \
            self.onTextDeleted
        listeners["object:text-changed:insert"]             = \
            self.onTextInserted
        listeners["object:active-descendant-changed"]       = \
            self.onActiveDescendantChanged
        listeners["object:children-changed:add"]            = \
            self.onChildrenAdded
        listeners["object:children-changed:remove"]         = \
            self.onChildrenRemoved
        listeners["object:state-changed:active"]            = \
            self.onActiveChanged
        listeners["object:state-changed:busy"]              = \
            self.onBusyChanged
        listeners["object:state-changed:focused"]           = \
            self.onFocusedChanged
        listeners["object:state-changed:showing"]           = \
            self.onShowingChanged
        listeners["object:state-changed:checked"]           = \
            self.onCheckedChanged
        listeners["object:state-changed:pressed"]           = \
            self.onPressedChanged
        listeners["object:state-changed:indeterminate"]     = \
            self.onIndeterminateChanged
        listeners["object:state-changed:expanded"]          = \
            self.onExpandedChanged
        listeners["object:state-changed:selected"]          = \
            self.onSelectedChanged
        listeners["object:state-changed:sensitive"]         = \
            self.onSensitiveChanged
        listeners["object:text-attributes-changed"]         = \
            self.onTextAttributesChanged
        listeners["object:text-selection-changed"]          = \
            self.onTextSelectionChanged
        listeners["object:selection-changed"]               = \
            self.onSelectionChanged
        listeners["object:property-change:accessible-value"] = \
            self.onValueChanged
        listeners["object:value-changed"]                   = \
            self.onValueChanged
        listeners["object:column-reordered"]                = \
            self.onColumnReordered
        listeners["object:row-reordered"]                   = \
            self.onRowReordered
        listeners["window:activate"]                        = \
            self.onWindowActivated
        listeners["window:deactivate"]                      = \
            self.onWindowDeactivated
        listeners["window:create"]                          = \
            self.onWindowCreated
        listeners["window:destroy"]                          = \
            self.onWindowDestroyed

        return listeners

    def __getDesktopBindings(self):
        """Returns an instance of keybindings.KeyBindings that use the
        numeric keypad for focus tracking and flat review.
        """

        import cthulhu.desktop_keyboardmap as desktop_keyboardmap
        keyBindings = keybindings.KeyBindings()
        keyBindings.load(desktop_keyboardmap.keymap, self.inputEventHandlers)
        return keyBindings

    def __getLaptopBindings(self):
        """Returns an instance of keybindings.KeyBindings that use the
        the main keyboard keys for focus tracking and flat review.
        """

        import cthulhu.laptop_keyboardmap as laptop_keyboardmap
        keyBindings = keybindings.KeyBindings()
        keyBindings.load(laptop_keyboardmap.keymap, self.inputEventHandlers)
        return keyBindings

    def getExtensionBindings(self):
        keyBindings = keybindings.KeyBindings()

        bindings = self.notificationPresenter.get_bindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        layout = _settingsManager.getSetting('keyboardLayout')
        isDesktop = layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP
        bindings = self.flatReviewPresenter.get_bindings(isDesktop)
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.whereAmIPresenter.get_bindings(isDesktop)
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.learnModePresenter.get_bindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.speechAndVerbosityManager.get_bindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.dateAndTimePresenter.get_bindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.objectNavigator.get_bindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.bookmarks.get_bindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.mouseReviewer.get_bindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.actionPresenter.get_bindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        return keyBindings

    def getKeyBindings(self):
        """Defines the key bindings for this script.

        Returns an instance of keybindings.KeyBindings.
        """

        keyBindings = script.Script.getKeyBindings(self)

        bindings = self.getDefaultKeyBindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.getToolkitKeyBindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.getAppKeyBindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        bindings = self.getExtensionBindings()
        for keyBinding in bindings.keyBindings:
            keyBindings.add(keyBinding)

        try:
            keyBindings = _settingsManager.overrideKeyBindings(self, keyBindings)
        except Exception as error:
            tokens = ["DEFAULT: Exception when overriding keybindings in", self, ":", error]
            debug.printTokens(debug.LEVEL_WARNING, tokens, True)

        return keyBindings

    def getDefaultKeyBindings(self):
        """Returns the default script's keybindings, i.e. without any of
        the toolkit or application specific commands added."""

        keyBindings = keybindings.KeyBindings()

        layout = _settingsManager.getSetting('keyboardLayout')
        if layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP:
            for keyBinding in self.__getDesktopBindings().keyBindings:
                keyBindings.add(keyBinding)
        else:
            for keyBinding in self.__getLaptopBindings().keyBindings:
                keyBindings.add(keyBinding)

        import cthulhu.common_keyboardmap as common_keyboardmap
        keyBindings.load(common_keyboardmap.keymap, self.inputEventHandlers)

        return keyBindings

    def getBrailleBindings(self):
        """Defines the braille bindings for this script.

        Returns a dictionary where the keys are BrlTTY commands and the
        values are InputEventHandler instances.
        """

        msg = 'DEFAULT: Getting braille bindings.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        brailleBindings = script.Script.getBrailleBindings(self)
        try:
            brailleBindings[braille.brlapi.KEY_CMD_HWINLT]     = \
                self.inputEventHandlers["panBrailleLeftHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_FWINLT]     = \
                self.inputEventHandlers["panBrailleLeftHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_FWINLTSKIP] = \
                self.inputEventHandlers["panBrailleLeftHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_HWINRT]     = \
                self.inputEventHandlers["panBrailleRightHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_FWINRT]     = \
                self.inputEventHandlers["panBrailleRightHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_FWINRTSKIP] = \
                self.inputEventHandlers["panBrailleRightHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_HOME]       = \
                self.inputEventHandlers["goBrailleHomeHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_SIXDOTS]     = \
                self.inputEventHandlers["contractedBrailleHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_ROUTE]     = \
                self.inputEventHandlers["processRoutingKeyHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_CUTBEGIN]   = \
                self.inputEventHandlers["processBrailleCutBeginHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_CUTLINE]   = \
                self.inputEventHandlers["processBrailleCutLineHandler"]
            brailleBindings[braille.brlapi.KEY_CMD_HOME] = \
                self.inputEventHandlers["goBrailleHomeHandler"]
        except AttributeError:
            tokens = ["DEFAULT: Braille bindings unavailable in", self]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
        except Exception as error:
            tokens = ["DEFAULT: Exception getting braille bindings in", self, ":", error]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)

        reviewBindings = self.flatReviewPresenter.get_braille_bindings()
        brailleBindings.update(reviewBindings)

        msg = 'DEFAULT: Finished getting braille bindings.'
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        return brailleBindings

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

        self._inSayAll = False
        self._sayAllIsInterrupted = False
        self.pointOfReference = {}

        self.removeKeyGrabs()

    def getEnabledKeyBindings(self):
        """ Returns the key bindings that are currently active. """
        return self.getKeyBindings().getBoundBindings()

    def addKeyGrabs(self):
        """ Sets up the key grabs currently needed by this script. """
        if cthulhu_state.device is None:
            return
        msg = "DEFAULT: adding key grabs"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        bound = self.getEnabledKeyBindings()
        for b in bound:
            for id in cthulhu.addKeyGrab(b):
                self.grab_ids.append(id)

    def removeKeyGrabs(self):
        """ Removes this script's AT-SPI key grabs. """
        msg = "DEFAULT: removing key grabs"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        for id in self.grab_ids:
            cthulhu.removeKeyGrab(id)
        self.grab_ids = []

    def refreshKeyGrabs(self):
        """ Refreshes the enabled key grabs for this script. """
        # TODO: Should probably avoid removing key grabs and re-adding them.
        # Otherwise, a key could conceivably leak through while the script is
        # in the process of updating the bindings.
        self.removeKeyGrabs()
        self.addKeyGrabs()

    def registerEventListeners(self):
        super().registerEventListeners()
        self.utilities.connectToClipboard()

    def deregisterEventListeners(self):
        super().deregisterEventListeners()
        self.utilities.disconnectFromClipboard()

    def _saveFocusedObjectInfo(self, obj):
        """Saves some basic information about obj. Note that this method is
        intended to be called primarily (if not only) by locusOfFocusChanged().
        It is expected that accessible event callbacks will update the point
        of reference data specific to that event. The goal here is to weed
        out duplicate events."""

        if not obj:
            return

        # We want to save the name because some apps and toolkits emit name
        # changes after the focus or selection has changed, even though the
        # name has not.
        name = AXObject.get_name(obj)
        names = self.pointOfReference.get('names', {})
        names[hash(obj)] = name
        if cthulhu_state.activeWindow:
            names[hash(cthulhu_state.activeWindow)] = AXObject.get_name(cthulhu_state.activeWindow)
        self.pointOfReference['names'] = names

        descriptions = self.pointOfReference.get('descriptions', {})
        descriptions[hash(obj)] = AXObject.get_description(obj)
        self.pointOfReference['descriptions'] = descriptions

        # We want to save the offset for text objects because some apps and
        # toolkits emit caret-moved events immediately after a text object
        # gains focus, even though the caret has not actually moved.
        try:
            text = obj.queryText()
            caretOffset = text.caretOffset
        except Exception:
            pass
        else:
            self._saveLastCursorPosition(obj, max(0, caretOffset))
            self.utilities.updateCachedTextSelection(obj)

        # We want to save the current row and column of a newly focused
        # or selected table cell so that on subsequent cell focus/selection
        # we only present the changed location.
        row, column = self.utilities.coordinatesForCell(obj, findCellAncestor=True)
        self.pointOfReference['lastColumn'] = column
        self.pointOfReference['lastRow'] = row

        self.pointOfReference['checkedChange'] = hash(obj), AXUtilities.is_checked(obj)
        self.pointOfReference['selectedChange'] = hash(obj), AXUtilities.is_selected(obj)
        self.pointOfReference['expandedChange'] = hash(obj), AXUtilities.is_expanded(obj)

    def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus):
        """Called when the visual object with focus changes.

        Arguments:
        - event: if not None, the Event that caused the change
        - oldLocusOfFocus: Accessible that is the old locus of focus
        - newLocusOfFocus: Accessible that is the new locus of focus
        """

        self.utilities.presentFocusChangeReason()

        if not newLocusOfFocus:
            cthulhu_state.noFocusTimeStamp = time.time()
            return

        if AXUtilities.is_defunct(newLocusOfFocus):
            return

        if oldLocusOfFocus == newLocusOfFocus:
            msg = 'DEFAULT: old focus == new focus'
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        # Don't apply the is-same-object heuristic in the case of table cells.
        # One scenario is email client message lists. When you delete a message
        # and land on the next one, the cells likely occupy the same space,
        # have the same role, and might even have the same name, same path, etc.
        if not AXUtilities.is_table_cell(oldLocusOfFocus) \
           and not AXUtilities.is_table_cell(newLocusOfFocus) \
           and self.utilities.isSameObject(oldLocusOfFocus, newLocusOfFocus):
            tokens = ["DEFAULT: old focus", oldLocusOfFocus,
                      "believed to be same as new focus", newLocusOfFocus]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        try:
            if self.findCommandRun:
                # Then the Cthulhu Find dialog has just given up focus
                # to the original window.  We don't want to speak
                # the window title, current line, etc.
                return
        except Exception:
            pass

        if self.flatReviewPresenter.is_active():
            self.flatReviewPresenter.quit()

        if self.learnModePresenter.is_active():
            self.learnModePresenter.quit()

        topLevel = self.utilities.topLevelObject(newLocusOfFocus)
        if cthulhu_state.activeWindow != topLevel:
            cthulhu.setActiveWindow(topLevel)

        self.updateBraille(newLocusOfFocus)

        utterances = self.speechGenerator.generateSpeech(
            newLocusOfFocus,
            priorObj=oldLocusOfFocus)

        if self.utilities.shouldInterruptForLocusOfFocusChange(
           oldLocusOfFocus, newLocusOfFocus, event):
            self.presentationInterrupt()
        speech.speak(utterances, interrupt=False)
        cthulhu.emitRegionChanged(newLocusOfFocus)
        self._saveFocusedObjectInfo(newLocusOfFocus)

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

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

        _settingsManager.loadAppSettings(self)
        braille.checkBrailleSetting()
        braille.setupKeyRanges(self.brailleBindings.keys())
        speech.checkSpeechSetting()
        self.speechAndVerbosityManager.update_punctuation_level()
        self.speechAndVerbosityManager.update_capitalization_style()

        # Gtk 4 requrns "GTK", while older versions return "gtk"
        # TODO: move this to a toolkit-specific script
        if self.app is not None and self.app.toolkitName == "GTK" and self.app.toolkitVersion > "4":
            cthulhu.setKeyHandling(True)
        else:
            cthulhu.setKeyHandling(False)

        self.addKeyGrabs()

        tokens = ["DEFAULT: Script for", self.app, "activated"]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

    def updateBraille(self, obj, **args):
        """Updates the braille display to show the give object.

        Arguments:
        - obj: the Accessible
        """

        if not _settingsManager.getSetting('enableBraille') \
           and not _settingsManager.getSetting('enableBrailleMonitor'):
            debug.printMessage(debug.LEVEL_INFO, "BRAILLE: update disabled", True)
            return

        if not obj:
            return

        result, focusedRegion = self.brailleGenerator.generateBraille(obj, **args)
        if not result:
            return

        self.clearBraille()
        line = self.getNewBrailleLine()
        braille.addLine(line)
        self.addBrailleRegionsToLine(result, line)

        extraRegion = args.get('extraRegion')
        if extraRegion:
            self.addBrailleRegionToLine(extraRegion, line)
            self.setBrailleFocus(extraRegion)
        else:
            self.setBrailleFocus(focusedRegion)

        self.refreshBraille(True)

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

    def bypassNextCommand(self, inputEvent=None):
        """Causes the next keyboard command to be ignored by Cthulhu
        and passed along to the current application.

        Returns True to indicate the input event has been consumed.
        """

        self.presentMessage(messages.BYPASS_MODE_ENABLED)
        cthulhu_state.bypassNextCommand = True
        self.removeKeyGrabs()
        return True

    def findNext(self, inputEvent):
        """Searches forward for the next instance of the string
        searched for via the Cthulhu Find dialog.  Other than direction
        and the starting point, the search options initially specified
        (case sensitivity, window wrap, and full/partial match) are
        preserved.
        """

        lastQuery = find.getLastQuery()
        if lastQuery:
            lastQuery.searchBackwards = False
            lastQuery.startAtTop = False
            self.find(lastQuery)
        else:
            cthulhu.showFindGUI()

    def findPrevious(self, inputEvent):
        """Searches backwards for the next instance of the string
        searched for via the Cthulhu Find dialog.  Other than direction
        and the starting point, the search options initially specified
        (case sensitivity, window wrap, and full/or partial match) are
        preserved.
        """

        lastQuery = find.getLastQuery()
        if lastQuery:
            lastQuery.searchBackwards = True
            lastQuery.startAtTop = False
            self.find(lastQuery)
        else:
            cthulhu.showFindGUI()

    def panBrailleLeft(self, inputEvent=None, panAmount=0):
        """Pans the braille display to the left.  If panAmount is non-zero,
        the display is panned by that many cells.  If it is 0, the display
        is panned one full display width.  In flat review mode, panning
        beyond the beginning will take you to the end of the previous line.

        In focus tracking mode, the cursor stays at its logical position.
        In flat review mode, the review cursor moves to character
        associated with cell 0."""

        if isinstance(inputEvent, input_event.KeyboardEvent) \
           and not _settingsManager.getSetting('enableBraille') \
           and not _settingsManager.getSetting('enableBrailleMonitor'):
            msg = "DEFAULT: panBrailleLeft command requires braille or braille monitor"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if self.flatReviewPresenter.is_active():
            if self.isBrailleBeginningShowing():
                self.flatReviewPresenter.go_start_of_line(self, inputEvent)
                self.flatReviewPresenter.go_previous_character(self, inputEvent)
            else:
                self.panBrailleInDirection(panAmount, panToLeft=True)

            self._setFlatReviewContextToBeginningOfBrailleDisplay()
            self.targetCursorCell = 1
            self.updateBrailleReview(self.targetCursorCell)
        elif self.isBrailleBeginningShowing() and cthulhu_state.locusOfFocus \
             and self.utilities.isTextArea(cthulhu_state.locusOfFocus):

            # If we're at the beginning of a line of a multiline text
            # area, then force it's caret to the end of the previous
            # line.  The assumption here is that we're currently
            # viewing the line that has the caret -- which is a pretty
            # good assumption for focus tacking mode.  When we set the
            # caret position, we will get a caret event, which will
            # then update the braille.
            #
            text = cthulhu_state.locusOfFocus.queryText()
            [lineString, startOffset, endOffset] = text.getTextAtOffset(
                text.caretOffset,
                Atspi.TextBoundaryType.LINE_START)
            movedCaret = False
            if startOffset > 0:
                movedCaret = text.setCaretOffset(startOffset - 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 movedCaret and AXUtilities.is_terminal(cthulhu_state.locusOfFocus):
                context = self.getFlatReviewContext()
                context.goBegin(flat_review.Context.LINE)
                self.flatReviewPresenter.go_previous_character(self, inputEvent)
        else:
            self.panBrailleInDirection(panAmount, panToLeft=True)
            # We might be panning through a flashed message.
            #
            braille.resetFlashTimer()
            self.refreshBraille(False, stopFlash=False)

        return True

    def panBrailleLeftOneChar(self, inputEvent=None):
        """Nudges the braille display one character to the left.

        In focus tracking mode, the cursor stays at its logical position.
        In flat review mode, the review cursor moves to character
        associated with cell 0."""

        self.panBrailleLeft(inputEvent, 1)

    def panBrailleRight(self, inputEvent=None, panAmount=0):
        """Pans the braille display to the right.  If panAmount is non-zero,
        the display is panned by that many cells.  If it is 0, the display
        is panned one full display width.  In flat review mode, panning
        beyond the end will take you to the beginning of the next line.

        In focus tracking mode, the cursor stays at its logical position.
        In flat review mode, the review cursor moves to character
        associated with cell 0."""

        if isinstance(inputEvent, input_event.KeyboardEvent) \
           and not _settingsManager.getSetting('enableBraille') \
           and not _settingsManager.getSetting('enableBrailleMonitor'):
            msg = "DEFAULT: panBrailleRight command requires braille or braille monitor"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if self.flatReviewPresenter.is_active():
            if self.isBrailleEndShowing():
                self.flatReviewPresenter.go_end_of_line(self, inputEvent)
                # Reviewing the next character also updates the braille output
                # and refreshes the display.
                self.flatReviewPresenter.go_next_character(self, inputEvent)
                return
            self.panBrailleInDirection(panAmount, panToLeft=False)
            self._setFlatReviewContextToBeginningOfBrailleDisplay()
            self.targetCursorCell = 1
            self.updateBrailleReview(self.targetCursorCell)
        elif self.isBrailleEndShowing() and cthulhu_state.locusOfFocus \
             and self.utilities.isTextArea(cthulhu_state.locusOfFocus):
            # If we're at the end of a line of a multiline text area, then
            # force it's caret to the beginning of the next line.  The
            # assumption here is that we're currently viewing the line that
            # has the caret -- which is a pretty good assumption for focus
            # tacking mode.  When we set the caret position, we will get a
            # caret event, which will then update the braille.
            #
            text = cthulhu_state.locusOfFocus.queryText()
            [lineString, startOffset, endOffset] = text.getTextAtOffset(
                text.caretOffset,
                Atspi.TextBoundaryType.LINE_START)
            if endOffset < text.characterCount:
                text.setCaretOffset(endOffset)
        else:
            self.panBrailleInDirection(panAmount, panToLeft=False)
            # We might be panning through a flashed message.
            #
            braille.resetFlashTimer()
            self.refreshBraille(False, stopFlash=False)

        return True

    def panBrailleRightOneChar(self, inputEvent=None):
        """Nudges the braille display one character to the right.

        In focus tracking mode, the cursor stays at its logical position.
        In flat review mode, the review cursor moves to character
        associated with cell 0."""

        self.panBrailleRight(inputEvent, 1)

    def goBrailleHome(self, inputEvent=None):
        """Returns to the component with focus."""

        if self.flatReviewPresenter.is_active():
            self.flatReviewPresenter.quit()
            return True

        return braille.returnToRegionWithFocus(inputEvent)

    def setContractedBraille(self, inputEvent=None):
        """Toggles contracted braille."""

        self._setContractedBraille(inputEvent)
        return True

    def processRoutingKey(self, inputEvent=None):
        """Processes a cursor routing key."""

        braille.processRoutingKey(inputEvent)
        return True

    def processBrailleCutBegin(self, inputEvent=None):
        """Clears the selection and moves the caret offset in the currently
        active text area.
        """

        obj, caretOffset = self.getBrailleCaretContext(inputEvent)

        if caretOffset >= 0:
            self.utilities.clearTextSelection(obj)
            self.utilities.setCaretOffset(obj, caretOffset)

        return True

    def processBrailleCutLine(self, inputEvent=None):
        """Extends the text selection in the currently active text
        area and also copies the selected text to the system clipboard."""

        obj, caretOffset = self.getBrailleCaretContext(inputEvent)

        if caretOffset >= 0:
            self.utilities.adjustTextSelection(obj, caretOffset)
            texti = obj.queryText()
            startOffset, endOffset = texti.getSelection(0)
            self.utilities.setClipboardText(texti.getText(startOffset, endOffset))

        return True

    def routePointerToItem(self, inputEvent=None):
        """Moves the mouse pointer to the current item."""

        # Store the original location for scripts which want to restore
        # it later.
        #
        self.oldMouseCoordinates = self.utilities.absoluteMouseCoordinates()
        self.lastMouseRoutingTime = time.time()
        if self.flatReviewPresenter.is_active():
            self.flatReviewPresenter.route_pointer_to_object(self, inputEvent)
            return True

        if self.eventSynthesizer.route_to_character(cthulhu_state.locusOfFocus) \
           or self.eventSynthesizer.route_to_object(cthulhu_state.locusOfFocus):
            self.presentMessage(messages.MOUSE_MOVED_SUCCESS)
            return True

        full = messages.LOCATION_NOT_FOUND_FULL
        brief = messages.LOCATION_NOT_FOUND_BRIEF
        self.presentMessage(full, brief)
        return False

    def leftClickReviewItem(self, inputEvent=None):
        """Performs a left mouse button click on the current item."""

        if self.flatReviewPresenter.is_active():
            obj = self.flatReviewPresenter.get_current_object(self, inputEvent)
            if self.eventSynthesizer.try_all_clickable_actions(obj):
                return True
            return self.flatReviewPresenter.left_click_on_object(self, inputEvent)

        if self.eventSynthesizer.try_all_clickable_actions(cthulhu_state.locusOfFocus):
            return True

        if self.utilities.queryNonEmptyText(cthulhu_state.locusOfFocus):
            if self.eventSynthesizer.click_character(cthulhu_state.locusOfFocus, 1):
                return True

        if self.eventSynthesizer.click_object(cthulhu_state.locusOfFocus, 1):
            return True

        full = messages.LOCATION_NOT_FOUND_FULL
        brief = messages.LOCATION_NOT_FOUND_BRIEF
        self.presentMessage(full, brief)
        return False

    def rightClickReviewItem(self, inputEvent=None):
        """Performs a right mouse button click on the current item."""

        if self.flatReviewPresenter.is_active():
            self.flatReviewPresenter.right_click_on_object(self, inputEvent)
            return True

        if self.eventSynthesizer.click_character(cthulhu_state.locusOfFocus, 3):
            return True

        if self.eventSynthesizer.click_object(cthulhu_state.locusOfFocus, 3):
            return True

        full = messages.LOCATION_NOT_FOUND_FULL
        brief = messages.LOCATION_NOT_FOUND_BRIEF
        self.presentMessage(full, brief)
        return False

    def spellCurrentItem(self, itemString):
        """Spell the current flat review word or line.

        Arguments:
        - itemString: the string to spell.
        """

        for character in itemString:
            self.speakCharacter(character)

    def sayAll(self, inputEvent, obj=None, offset=None):
        obj = obj or cthulhu_state.locusOfFocus
        if not obj or AXObject.is_dead(obj):
            self.presentMessage(messages.LOCATION_NOT_FOUND_FULL)
            return True

        try:
            text = obj.queryText()
        except NotImplementedError:
            utterances = self.speechGenerator.generateSpeech(obj)
            utterances.extend(self.tutorialGenerator.getTutorial(obj, False))
            speech.speak(utterances)
        except AttributeError:
            pass
        else:
            if offset is None:
                offset = text.caretOffset
            speech.sayAll(self.textLines(obj, offset),
                          self.__sayAllProgressCallback)

        return True

    def cycleSettingsProfile(self, inputEvent=None):
        """Cycle through the user's existing settings profiles."""

        profiles = _settingsManager.availableProfiles()
        if not (profiles and profiles[0]):
            self.presentMessage(messages.PROFILE_NOT_FOUND)
            return True

        def isMatch(x):
            return x is not None and x[1] == _settingsManager.getProfile()

        current = list(filter(isMatch, profiles))[0]
        try:
            name, profileID = profiles[profiles.index(current) + 1]
        except IndexError:
            name, profileID = profiles[0]

        _settingsManager.setProfile(profileID, updateLocale=True)

        braille.checkBrailleSetting()

        speech.shutdown()
        speech.init()

        # TODO: This is another "too close to code freeze" hack to cause the
        # command names to be presented in the correct language.
        self.setupInputEventHandlers()

        self.presentMessage(messages.PROFILE_CHANGED % name, name)
        return True

    def cycleDebugLevel(self, inputEvent=None):
        levels = [debug.LEVEL_ALL, "all",
                  debug.LEVEL_FINEST, "finest",
                  debug.LEVEL_FINER, "finer",
                  debug.LEVEL_FINE, "fine",
                  debug.LEVEL_CONFIGURATION, "configuration",
                  debug.LEVEL_INFO, "info",
                  debug.LEVEL_WARNING, "warning",
                  debug.LEVEL_SEVERE, "severe",
                  debug.LEVEL_OFF, "off"]

        try:
            levelIndex = levels.index(debug.debugLevel) + 2
        except Exception:
            levelIndex = 0
        else:
            if levelIndex >= len(levels):
                levelIndex = 0

        debug.debugLevel = levels[levelIndex]
        briefMessage = levels[levelIndex + 1]
        fullMessage =  f"Debug level {briefMessage}."
        self.presentMessage(fullMessage, briefMessage)

        return True

    ########################################################################
    #                                                                      #
    # AT-SPI OBJECT EVENT HANDLERS                                         #
    #                                                                      #
    ########################################################################

    def noOp(self, event):
        """Just here to capture events.

        Arguments:
        - event: the Event
        """
        pass

    def onActiveChanged(self, event):
        """Callback for object:state-changed:active accessibility events."""

        window = event.source
        if AXUtilities.is_application(AXObject.get_parent(event.source)):
            window = AXObject.find_real_app_and_window_for(event.source)[1]

        if AXUtilities.is_dialog_or_alert(window) or AXUtilities.is_frame(window):
            if event.detail1 and not self.utilities.canBeActiveWindow(window):
                return

            sourceIsActiveWindow = self.utilities.isSameObject(window, cthulhu_state.activeWindow)
            if sourceIsActiveWindow and not event.detail1:
                if self.utilities.inMenu():
                    msg = "DEFAULT: Ignoring event. In menu."
                    debug.printMessage(debug.LEVEL_INFO, msg, True)
                    return

                if not self.utilities.eventIsUserTriggered(event):
                    msg = "DEFAULT: Not clearing state. Event is not user triggered."
                    debug.printMessage(debug.LEVEL_INFO, msg, True)
                    return

                msg = "DEFAULT: Event is for active window. Clearing state."
                debug.printMessage(debug.LEVEL_INFO, msg, True)
                cthulhu.setActiveWindow(None)
                return

            if not sourceIsActiveWindow and event.detail1:
                msg = "DEFAULT: Updating active window."
                debug.printMessage(debug.LEVEL_INFO, msg, True)
                cthulhu.setActiveWindow(window, alsoSetLocusOfFocus=True, notifyScript=True)

        if self.findCommandRun:
            self.findCommandRun = False
            self.find()

    def onActiveDescendantChanged(self, event):
        """Callback for object:active-descendant-changed accessibility events."""

        if not event.any_data:
            msg = "DEFAULT: Ignoring event. No any_data."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if not AXUtilities.is_focused(event.source) \
           and not AXUtilities.is_focused(event.any_data):
            msg = "DEFAULT: Ignoring event. Neither source nor child have focused state."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if self.stopSpeechOnActiveDescendantChanged(event):
            self.presentationInterrupt()

        tokens = ["DEFAULT: Setting locus of focus to any_data", event.any_data]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)
        cthulhu.setLocusOfFocus(event, event.any_data)

    def onBusyChanged(self, event):
        """Callback for object:state-changed:busy accessibility events."""
        pass

    def onCheckedChanged(self, event):
        """Callback for object:state-changed:checked accessibility events."""

        if not self.utilities.isSameObject(event.source, cthulhu_state.locusOfFocus):
            return

        if AXUtilities.is_expandable(event.source):
            return

        # Radio buttons normally change their state when you arrow to them,
        # so we handle the announcement of their state changes in the focus
        # handling code.  However, we do need to handle radio buttons where
        # the user needs to press the space key to select them.
        if AXObject.get_role(event.source) == Atspi.Role.RADIO_BUTTON:
            eventString, mods = self.utilities.lastKeyAndModifiers()
            if eventString not in [" ", "space"]:
                return

        oldObj, oldState = self.pointOfReference.get('checkedChange', (None, 0))
        if hash(oldObj) == hash(event.source) and oldState == event.detail1:
            return

        self.presentObject(event.source, alreadyFocused=True, interrupt=True)
        self.pointOfReference['checkedChange'] = hash(event.source), event.detail1

    def onChildrenAdded(self, event):
        """Callback for object:children-changed:add accessibility events."""

        AXObject.clear_cache_now("children-changed event.")

    def onChildrenRemoved(self, event):
        """Callback for object:children-changed:remove accessibility events."""

        AXObject.clear_cache_now("children-changed event.")

    def onCaretMoved(self, event):
        """Callback for object:text-caret-moved accessibility events."""

        obj, offset = self.pointOfReference.get("lastCursorPosition", (None, -1))
        if offset == event.detail1 and obj == event.source:
            msg = "DEFAULT: Event is for last saved cursor position"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if not AXUtilities.is_showing(event.source):
            msg = "DEFAULT: Event source is not showing. Clearing cache."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            AXObject.clear_cache(obj)
            if not AXUtilities.is_showing(event.source):
                msg = "DEFAULT: Event source is still not showing."
                debug.printMessage(debug.LEVEL_INFO, msg, True)
                if not self.utilities.presentEventFromNonShowingObject(event):
                    return

        if event.source != cthulhu_state.locusOfFocus and AXUtilities.is_focused(event.source):
            topLevelObject = self.utilities.topLevelObject(event.source)
            if self.utilities.isSameObject(cthulhu_state.activeWindow, topLevelObject):
                tokens = ["DEFAULT: Updating locusOfFocus from", cthulhu_state.locusOfFocus,
                          "to", event.source]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                cthulhu.setLocusOfFocus(event, event.source, False)
            else:
                tokens = ["DEFAULT: Source window (", topLevelObject, ") is not active window (",
                          cthulhu_state.activeWindow]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if event.source != cthulhu_state.locusOfFocus:
            tokens = ["DEFAULT: Event source (", event.source, ") is not locusOfFocus (",
                      cthulhu_state.locusOfFocus]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        if self.flatReviewPresenter.is_active():
            self.flatReviewPresenter.quit()

        text = event.source.queryText()
        try:
            text.caretOffset
        except Exception as error:
            tokens = ["DEFAULT: Exception getting caretOffset for", event.source, ":", error]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        self._saveLastCursorPosition(event.source, text.caretOffset)
        if text.getNSelections() > 0:
            msg = "DEFAULT: Event source has text selections"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self.utilities.handleTextSelectionChange(event.source)
            return
        else:
            start, end, string = self.utilities.getCachedTextSelection(obj)
            if string and self.utilities.handleTextSelectionChange(obj):
                msg = "DEFAULT: Event handled as text selection change"
                debug.printMessage(debug.LEVEL_INFO, msg, True)
                return

        msg = "DEFAULT: Presenting text at new caret position"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self._presentTextAtNewCaretPosition(event)

    def onDescriptionChanged(self, event):
        """Callback for object:property-change:accessible-description events."""

        obj = event.source
        descriptions = self.pointOfReference.get('description', {})
        oldDescription = descriptions.get(hash(obj))
        if oldDescription == event.any_data:
            tokens = ["DEFAULT: Old description (", oldDescription, ") is the same as new one"]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        if obj != cthulhu_state.locusOfFocus:
            msg = "DEFAULT: Event is for object other than the locusOfFocus"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        descriptions[hash(obj)] = event.any_data
        self.pointOfReference['descriptions'] = descriptions
        if event.any_data:
            self.presentMessage(event.any_data)

    def onDocumentReload(self, event):
        """Callback for document:reload accessibility events."""

        pass

    def onDocumentLoadComplete(self, event):
        """Callback for document:load-complete accessibility events."""

        pass

    def onDocumentLoadStopped(self, event):
        """Callback for document:load-stopped accessibility events."""

        pass

    def onExpandedChanged(self, event):
        """Callback for object:state-changed:expanded accessibility events."""

        if not self.utilities.isPresentableExpandedChangedEvent(event):
            return

        obj = event.source
        oldObj, oldState = self.pointOfReference.get('expandedChange', (None, 0))
        if hash(oldObj) == hash(obj) and oldState == event.detail1:
            return

        self.presentObject(obj, alreadyFocused=True, interrupt=True)
        self.pointOfReference['expandedChange'] = hash(obj), event.detail1

        details = self.utilities.detailsContentForObject(obj)
        for detail in details:
            self.speakMessage(detail, interrupt=False)

    def onIndeterminateChanged(self, event):
        """Callback for object:state-changed:indeterminate accessibility events."""

        # If this state is cleared, the new state will become checked or unchecked
        # and we should get object:state-changed:checked events for those cases.
        # Therefore, if the state is not now indeterminate/partially checked,
        # ignore this event.
        if not event.detail1:
            return

        obj = event.source
        if not self.utilities.isSameObject(obj, cthulhu_state.locusOfFocus):
            return

        oldObj, oldState = self.pointOfReference.get('indeterminateChange', (None, 0))
        if hash(oldObj) == hash(obj) and oldState == event.detail1:
            return

        self.presentObject(obj, alreadyFocused=True, interrupt=True)
        self.pointOfReference['indeterminateChange'] = hash(obj), event.detail1

    def onMouseButton(self, event):
        """Callback for mouse:button events."""

        mouseEvent = input_event.MouseButtonEvent(event)
        cthulhu_state.lastInputEvent = mouseEvent
        if not mouseEvent.pressed:
            return

        windowChanged = cthulhu_state.activeWindow != mouseEvent.window
        if windowChanged:
            cthulhu.setActiveWindow(mouseEvent.window, alsoSetLocusOfFocus=True)

        self.presentationInterrupt()
        if AXUtilities.is_focused(mouseEvent.obj):
            cthulhu.setLocusOfFocus(None, mouseEvent.obj, windowChanged)

    def onAnnouncement(self, event):
        """Callback for object:announcement events."""

        if isinstance(event.any_data, str):
            self.presentMessage(event.any_data)

    def onNameChanged(self, event):
        """Callback for object:property-change:accessible-name events."""

        names = self.pointOfReference.get('names', {})
        oldName = names.get(hash(event.source))
        if oldName == event.any_data:
            tokens = ["DEFAULT: Old name (", oldName, ") is the same as new name"]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        if AXUtilities.is_combo_box(event.source) or AXUtilities.is_table_cell(event.source):
            msg = "DEFAULT: Event is redundant notification for this role"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if AXUtilities.is_frame(event.source):
            if event.source != cthulhu_state.activeWindow:
                msg = "DEFAULT: Event is for frame other than the active window"
                debug.printMessage(debug.LEVEL_INFO, msg, True)
                return
        elif event.source != cthulhu_state.locusOfFocus:
            msg = "DEFAULT: Event is for object other than the locusOfFocus"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        names[hash(event.source)] = event.any_data
        self.pointOfReference['names'] = names
        if event.any_data:
            self.presentMessage(event.any_data)

    def onPressedChanged(self, event):
        """Callback for object:state-changed:pressed accessibility events."""

        obj = event.source
        if not self.utilities.isSameObject(obj, cthulhu_state.locusOfFocus):
            return

        oldObj, oldState = self.pointOfReference.get('pressedChange', (None, 0))
        if hash(oldObj) == hash(obj) and oldState == event.detail1:
            return

        self.presentObject(obj, alreadyFocused=True, interrupt=True)
        self.pointOfReference['pressedChange'] = hash(obj), event.detail1

    def onSelectedChanged(self, event):
        """Callback for object:state-changed:selected accessibility events."""

        AXObject.clear_cache(event.source)
        if not AXUtilities.is_focused(event.source):
            msg = "DEFAULT: Event is not toggling of currently-focused object"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if not self.utilities.isSameObject(cthulhu_state.locusOfFocus, event.source):
            tokens = ["DEFAULT: Event is not for locusOfFocus", cthulhu_state.locusOfFocus]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        if _settingsManager.getSetting('onlySpeakDisplayedText'):
            return

        isSelected = AXUtilities.is_selected(event.source)
        if isSelected != event.detail1:
            msg = "DEFAULT: Bogus event: detail1 doesn't match state"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        oldObj, oldState = self.pointOfReference.get('selectedChange', (None, 0))
        if hash(oldObj) == hash(event.source) and oldState == event.detail1:
            msg = "DEFAULT: Duplicate or spam event"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        announceState = False
        keyString, mods = self.utilities.lastKeyAndModifiers()
        if keyString == "space":
            announceState = True
        elif keyString in ["Down", "Up"] and AXUtilities.is_table_cell(event.source):
            announceState = isSelected

        if not announceState:
            return

        # TODO - JD: Unlike the other state-changed callbacks, it seems unwise
        # to call generateSpeech() 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:
            self.speakMessage(messages.TEXT_SELECTED, interrupt=False)
        else:
            self.speakMessage(messages.TEXT_UNSELECTED, interrupt=False)

        self.pointOfReference['selectedChange'] = hash(event.source), event.detail1

    def onSelectionChanged(self, event):
        """Callback for object:selection-changed accessibility events."""

        if self.utilities.handlePasteLocusOfFocusChange():
            if self.utilities.topLevelObjectIsActiveAndCurrent(event.source):
                cthulhu.setLocusOfFocus(event, event.source, False)
        elif self.utilities.handleContainerSelectionChange(event.source):
            return
        elif AXUtilities.manages_descendants(event.source):
            return

        # If the current item's selection is toggled, we'll present that
        # via the state-changed event.
        keyString, mods = self.utilities.lastKeyAndModifiers()
        if keyString == "space":
            return

        if AXUtilities.is_combo_box(event.source) and not AXUtilities.is_expanded(event.source):
            if AXUtilities.is_focused(self.utilities.getEntryForEditableComboBox(event.source)):
                return
        elif AXUtilities.is_page_tab_list(event.source) and self.flatReviewPresenter.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.
            self.flatReviewPresenter.quit()

        mouseReviewItem = self.mouseReviewer.getCurrentItem()
        selectedChildren = self.utilities.selectedChildren(event.source)
        for child in selectedChildren:
            if AXObject.find_ancestor(cthulhu_state.locusOfFocus, lambda x: x == child):
                tokens = ["DEFAULT: Child", child, "is ancestor of locusOfFocus"]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                self._saveFocusedObjectInfo(cthulhu_state.locusOfFocus)
                return

            if child == mouseReviewItem:
                tokens = ["DEFAULT: Child", child, "is current mouse review item"]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                continue

            if AXUtilities.is_page_tab(child) and cthulhu_state.locusOfFocus \
               and AXObject.get_name(child) == AXObject.get_name(cthulhu_state.locusOfFocus) \
               and not AXUtilities.is_focused(event.source):
                tokens = ["DEFAULT:", child, "'s selection redundant to", cthulhu_state.locusOfFocus]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                break

            if not self.utilities.isLayoutOnly(child):
                cthulhu.setLocusOfFocus(event, child)
                break

    def onSensitiveChanged(self, event):
        """Callback for object:state-changed:sensitive accessibility events."""
        pass

    def onFocus(self, event):
        """Callback for focus: accessibility events."""

        pass

    def onFocusedChanged(self, event):
        """Callback for object:state-changed:focused accessibility events."""

        if not event.detail1:
            return

        if not AXUtilities.is_focused(event.source):
            return

        obj = event.source
        window, dialog = self.utilities.frameAndDialog(obj)
        clearCache = window != cthulhu_state.activeWindow
        if window and not self.utilities.canBeActiveWindow(window, clearCache) and not dialog:
            return

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

        cthulhu.setLocusOfFocus(event, obj)

    def onShowingChanged(self, event):
        """Callback for object:state-changed:showing accessibility events."""

        obj = event.source
        role = AXObject.get_role(obj)
        if role == Atspi.Role.NOTIFICATION:
            if not event.detail1:
                return

            speech.speak(self.speechGenerator.generateSpeech(obj))
            msg = self.utilities.getNotificationContent(obj)
            self.displayBrailleMessage(msg, flashTime=settings.brailleFlashTime)
            self.notificationPresenter.save_notification(msg)
            return

        if role == Atspi.Role.TOOL_TIP:
            keyString, mods = self.utilities.lastKeyAndModifiers()
            if keyString != "F1" \
               and not _settingsManager.getSetting('presentToolTips'):
                return
            if event.detail1:
                self.presentObject(obj, interrupt=True)
                return
 
            if cthulhu_state.locusOfFocus and keyString == "F1":
                obj = cthulhu_state.locusOfFocus
                self.presentObject(obj, priorObj=event.source, interrupt=True)
                return

    def onTextAttributesChanged(self, event):
        """Callback for object:text-attributes-changed accessibility events."""

        if not self.utilities.isPresentableTextChangedEventForLocusOfFocus(event):
            return

        text = self.utilities.queryNonEmptyText(event.source)
        if not text:
            msg = "DEFAULT: Querying non-empty text returned None"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if _settingsManager.getSetting('speakMisspelledIndicator'):
            offset = text.caretOffset
            if not text.getText(offset, offset+1).isalnum():
                offset -= 1
            if self.utilities.isWordMisspelled(event.source, offset-1) \
               or self.utilities.isWordMisspelled(event.source, offset+1):
                self.speakMessage(messages.MISSPELLED)

    def onTextDeleted(self, event):
        """Callback for object:text-changed:delete accessibility events."""

        if not self.utilities.isPresentableTextChangedEventForLocusOfFocus(event):
            return

        self.utilities.handleUndoTextEvent(event)

        cthulhu.setLocusOfFocus(event, event.source, False)
        self.updateBraille(event.source)

        full, brief = "", ""
        if self.utilities.isClipboardTextChangedEvent(event):
            msg = "DEFAULT: Deletion is believed to be due to clipboard cut"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            full, brief = messages.CLIPBOARD_CUT_FULL, messages.CLIPBOARD_CUT_BRIEF
        elif self.utilities.isSelectedTextDeletionEvent(event):
            msg = "DEFAULT: Deletion is believed to be due to deleting selected text"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            full = messages.SELECTION_DELETED

        if full or brief:
            self.presentMessage(full, brief)
            self.utilities.updateCachedTextSelection(event.source)
            return

        string = self.utilities.deletedText(event)
        if self.utilities.isDeleteCommandTextDeletionEvent(event):
            msg = "DEFAULT: Deletion is believed to be due to Delete command"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            string = self.utilities.getCharacterAtOffset(event.source)
        elif self.utilities.isBackSpaceCommandTextDeletionEvent(event):
            msg = "DEFAULT: Deletion is believed to be due to BackSpace command"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
        else:
            msg = "DEFAULT: Event is not being presented due to lack of cause"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if len(string) == 1:
            self.speakCharacter(string)
        else:
            voice = self.speechGenerator.voice(string=string)
            string = self.utilities.adjustForRepeats(string)
            self.speakMessage(string, voice)

    def onTextInserted(self, event):
        """Callback for object:text-changed:insert accessibility events."""

        if not self.utilities.isPresentableTextChangedEventForLocusOfFocus(event):
            return

        self.utilities.handleUndoTextEvent(event)

        if event.source == cthulhu_state.locusOfFocus and self.utilities.isAutoTextEvent(event):
            self._saveFocusedObjectInfo(event.source)
        cthulhu.setLocusOfFocus(event, event.source, False)
        self.updateBraille(event.source)

        full, brief = "", ""
        if self.utilities.isClipboardTextChangedEvent(event):
            msg = "DEFAULT: Insertion is believed to be due to clipboard paste"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            full, brief = messages.CLIPBOARD_PASTED_FULL, messages.CLIPBOARD_PASTED_BRIEF
        elif self.utilities.isSelectedTextRestoredEvent(event):
            msg = "DEFAULT: Insertion is believed to be due to restoring selected text"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            full = messages.SELECTION_RESTORED

        if full or brief:
            self.presentMessage(full, brief)
            self.utilities.updateCachedTextSelection(event.source)
            return

        speakString = True

        # Because some implementations are broken.
        string = self.utilities.insertedText(event)

        if self.utilities.lastInputEventWasPageSwitch():
            msg = "DEFAULT: Insertion is believed to be due to page switch"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            speakString = False
        elif self.utilities.lastInputEventWasCommand():
            msg = "DEFAULT: Insertion is believed to be due to command"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
        elif self.utilities.isMiddleMouseButtonTextInsertionEvent(event):
            msg = "DEFAULT: Insertion is believed to be due to middle mouse button"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
        elif self.utilities.isEchoableTextInsertionEvent(event):
            msg = "DEFAULT: Insertion is believed to be echoable"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
        elif self.utilities.isAutoTextEvent(event):
            msg = "DEFAULT: Insertion is believed to be auto text event"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
        elif self.utilities.isSelectedTextInsertionEvent(event):
            msg = "DEFAULT: Insertion is also selected"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
        else:
            msg = "DEFAULT: Not speaking inserted string due to lack of cause"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            speakString = False

        if speakString:
            if len(string) == 1:
                self.speakCharacter(string)
            else:
                voice = self.speechGenerator.voice(obj=event.source, string=string)
                string = self.utilities.adjustForRepeats(string)
                self.speakMessage(string, voice)

        if len(string) != 1:
            return

        if _settingsManager.getSetting('enableEchoBySentence') \
           and self.echoPreviousSentence(event.source):
            return

        if _settingsManager.getSetting('enableEchoByWord'):
            self.echoPreviousWord(event.source)

    def onTextSelectionChanged(self, event):
        """Callback for object:text-selection-changed accessibility events."""

        obj = event.source

        # 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.

        self.utilities.handleTextSelectionChange(obj)
        self.updateBraille(obj)

    def onColumnReordered(self, event):
        """Callback for object:column-reordered accessibility events."""

        if not self.utilities.lastInputEventWasTableSort():
            return

        if event.source != self.utilities.getTable(cthulhu_state.locusOfFocus):
            return

        self.pointOfReference['last-table-sort-time'] = time.time()
        self.presentMessage(messages.TABLE_REORDERED_COLUMNS)

    def onRowReordered(self, event):
        """Callback for object:row-reordered accessibility events."""

        if not self.utilities.lastInputEventWasTableSort():
            return

        if event.source != self.utilities.getTable(cthulhu_state.locusOfFocus):
            return

        self.pointOfReference['last-table-sort-time'] = time.time()
        self.presentMessage(messages.TABLE_REORDERED_ROWS)

    def onValueChanged(self, event):
        """Called whenever an object's value changes.  Currently, the
        value changes for non-focused objects are ignored.

        Arguments:
        - event: the Event
        """

        obj = event.source
        role = AXObject.get_role(obj)

        try:
            value = obj.queryValue()
            currentValue = value.currentValue
        except NotImplementedError:
            tokens = ["DEFAULT:", obj, "doesn't implement AtspiValue"]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return
        except Exception:
            tokens = ["DEFAULT: Exception getting current value for", obj]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        if "oldValue" in self.pointOfReference \
           and (currentValue == self.pointOfReference["oldValue"]):
            return

        isProgressBarUpdate, msg = self.utilities.isProgressBarUpdate(obj)
        tokens = ["DEFAULT: Is progress bar update:", isProgressBarUpdate, ",", msg]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if not isProgressBarUpdate and obj != cthulhu_state.locusOfFocus:
            tokens = ["DEFAULT: Source != locusOfFocus (", cthulhu_state.locusOfFocus, ")"]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        if role == Atspi.Role.SPIN_BUTTON:
            self._saveFocusedObjectInfo(event.source)

        self.pointOfReference["oldValue"] = currentValue
        self.updateBraille(obj, isProgressBarUpdate=isProgressBarUpdate)
        speech.speak(self.speechGenerator.generateSpeech(
            obj, alreadyFocused=True, isProgressBarUpdate=isProgressBarUpdate))
        self.__play(self.soundGenerator.generateSound(
            obj, alreadyFocused=True, isProgressBarUpdate=isProgressBarUpdate))

    def onWindowActivated(self, event):
        """Called whenever a toplevel window is activated.

        Arguments:
        - event: the Event
        """

        window = AXObject.find_real_app_and_window_for(event.source)[1]
        if not self.utilities.canBeActiveWindow(window, False):
            return

        if self.utilities.isSameObject(window, cthulhu_state.activeWindow):
            msg = "DEFAULT: Event is for active window."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        self.pointOfReference = {}

        cthulhu.setActiveWindow(window)
        if self.utilities.isKeyGrabEvent(event):
            msg = "DEFAULT: Ignoring event. Likely from key grab."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if AXObject.get_child_count(window) == 1:
            child = AXObject.get_child(window, 0)
            if AXObject.get_role(child) == Atspi.Role.MENU:
                cthulhu.setLocusOfFocus(event, child)
                return

        cthulhu.setLocusOfFocus(event, cthulhu_state.activeWindow)

    def onWindowCreated(self, event):
        """Callback for window:create accessibility events."""

        pass

    def onWindowDestroyed(self, event):
        """Callback for window:destroy accessibility events."""

        pass

    def onWindowDeactivated(self, event):
        """Called whenever a toplevel window is deactivated.

        Arguments:
        - event: the Event
        """

        if self.utilities.inMenu():
            msg = "DEFAULT: Ignoring event. In menu."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if event.source != cthulhu_state.activeWindow:
            tokens = ["DEFAULT: Ignoring event. Not for active window",
                      cthulhu_state.activeWindow, "."]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return

        if self.utilities.isKeyGrabEvent(event):
            msg = "DEFAULT: Ignoring event. Likely from key grab."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if self.flatReviewPresenter.is_active():
            self.flatReviewPresenter.quit()

        if self.learnModePresenter.is_active():
            self.learnModePresenter.quit()

        self.pointOfReference = {}

        if not self.utilities.eventIsUserTriggered(event):
            msg = "DEFAULT: Not clearing state. Event is not user triggered."
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        msg = "DEFAULT: Clearing state."
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        cthulhu.setLocusOfFocus(event, None)
        cthulhu.setActiveWindow(None)
        _scriptManager.setActiveScript(None, "Window deactivated")

    def onClipboardContentsChanged(self, *args):
        if self.flatReviewPresenter.is_active():
            return

        if not self.utilities.objectContentsAreInClipboard():
            return

        if not self.utilities.topLevelObjectIsActiveAndCurrent():
            return

        if self.utilities.lastInputEventWasCopy():
            self.presentMessage(messages.CLIPBOARD_COPIED_FULL, messages.CLIPBOARD_COPIED_BRIEF)
            return

        if not self.utilities.lastInputEventWasCut():
            return

        if AXUtilities.is_editable(cthulhu_state.locusOfFocus):
            return

        self.presentMessage(messages.CLIPBOARD_CUT_FULL, messages.CLIPBOARD_CUT_BRIEF)

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

    def _presentTextAtNewCaretPosition(self, event, otherObj=None):
        obj = otherObj or event.source
        self.updateBrailleForNewCaretPosition(obj)
        if self._inSayAll:
            msg = "DEFAULT: Not presenting text because SayAll is active"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        if self.utilities.lastInputEventWasLineNav():
            msg = "DEFAULT: Presenting result of line nav"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self.sayLine(obj)
            return

        if self.utilities.lastInputEventWasWordNav():
            msg = "DEFAULT: Presenting result of word nav"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self.sayWord(obj)
            return

        if self.utilities.lastInputEventWasCharNav():
            msg = "DEFAULT: Presenting result of char nav"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self.sayCharacter(obj)
            return

        if self.utilities.lastInputEventWasPageNav():
            msg = "DEFAULT: Presenting result of page nav"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self.sayLine(obj)
            return

        if self.utilities.lastInputEventWasLineBoundaryNav():
            msg = "DEFAULT: Presenting result of line boundary nav"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self.sayCharacter(obj)
            return

        if self.utilities.lastInputEventWasFileBoundaryNav():
            msg = "DEFAULT: Presenting result of file boundary nav"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            self.sayLine(obj)
            return

        if self.utilities.lastInputEventWasPrimaryMouseClick() \
           or self.utilities.lastInputEventWasPrimaryMouseRelease():
            start, end, string = self.utilities.getCachedTextSelection(event.source)
            if not string:
                msg = "DEFAULT: Presenting result of primary mouse button event"
                debug.printMessage(debug.LEVEL_INFO, msg, True)
                self.sayLine(obj)
                return

    def _rewindSayAll(self, context, minCharCount=10):
        if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'):
            return False

        index = self._sayAllContexts.index(context)
        self._sayAllContexts = self._sayAllContexts[0:index]
        while self._sayAllContexts:
            context = self._sayAllContexts.pop()
            if context.endOffset - context.startOffset > minCharCount:
                break

        try:
            text = context.obj.queryText()
        except Exception:
            pass
        else:
            cthulhu.setLocusOfFocus(None, context.obj, notifyScript=False)
            text.setCaretOffset(context.startOffset)

        self.sayAll(None, context.obj, context.startOffset)
        return True

    def _fastForwardSayAll(self, context):
        if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'):
            return False

        try:
            text = context.obj.queryText()
        except Exception:
            pass
        else:
            cthulhu.setLocusOfFocus(None, context.obj, notifyScript=False)
            text.setCaretOffset(context.endOffset)

        self.sayAll(None, context.obj, context.endOffset)
        return True

    def __sayAllProgressCallback(self, context, progressType):
        # [[[TODO: WDW - this needs work.  Need to be able to manage
        # the monitoring of progress and couple that with both updating
        # the visual progress of what is being spoken as well as
        # positioning the cursor when speech has stopped.]]]
        #
        try:
            text = context.obj.queryText()
            char = text.getText(context.currentOffset, context.currentOffset+1)
        except Exception:
            return

        # Setting the caret at the offset of an embedded object results in
        # focus changes.
        if char == self.EMBEDDED_OBJECT_CHARACTER:
            return

        if progressType == speechserver.SayAllContext.PROGRESS:
            cthulhu.emitRegionChanged(
                context.obj, context.currentOffset, context.currentEndOffset, cthulhu.SAY_ALL)
            return

        if progressType == speechserver.SayAllContext.INTERRUPTED:
            if isinstance(cthulhu_state.lastInputEvent, input_event.KeyboardEvent):
                self._sayAllIsInterrupted = True
                lastKey = cthulhu_state.lastInputEvent.event_string
                if lastKey == "Down" and self._fastForwardSayAll(context):
                    return
                elif lastKey == "Up" and self._rewindSayAll(context):
                    return

            self._inSayAll = False
            self._sayAllContexts = []
            cthulhu.emitRegionChanged(context.obj, context.currentOffset)
            text.setCaretOffset(context.currentOffset)
        elif progressType == speechserver.SayAllContext.COMPLETED:
            cthulhu.setLocusOfFocus(None, context.obj, notifyScript=False)
            cthulhu.emitRegionChanged(context.obj, context.currentOffset, mode=cthulhu.SAY_ALL)
            text.setCaretOffset(context.currentOffset)

        # If there is a selection, clear it. See bug #489504 for more details.
        #
        if text.getNSelections() > 0:
            text.setSelection(0, context.currentOffset, context.currentOffset)

    def inSayAll(self, treatInterruptedAsIn=True):
        if self._inSayAll:
            msg = "DEFAULT: In SayAll"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return True

        if self._sayAllIsInterrupted:
            msg = "DEFAULT: SayAll is interrupted"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return treatInterruptedAsIn

        msg = "DEFAULT: Not in SayAll"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        return False

    def echoPreviousSentence(self, obj):
        """Speaks the sentence prior to the caret, as long as there is
        a sentence prior to the caret and there is no intervening sentence
        delimiter between the caret and the end of the sentence.

        The entry condition for this method is that the character
        prior to the current caret position is a sentence delimiter,
        and it's what caused this method to be called in the first
        place.

        Arguments:
        - obj: an Accessible object that implements the AccessibleText
        interface.
        """

        try:
            text = obj.queryText()
        except NotImplementedError:
            return False

        offset = text.caretOffset - 1
        previousOffset = text.caretOffset - 2
        if (offset < 0 or previousOffset < 0):
            return False

        [currentChar, startOffset, endOffset] = \
            text.getTextAtOffset(offset, Atspi.TextBoundaryType.CHAR)
        [previousChar, startOffset, endOffset] = \
            text.getTextAtOffset(previousOffset, Atspi.TextBoundaryType.CHAR)
        if not self.utilities.isSentenceDelimiter(currentChar, previousChar):
            return False

        # OK - we seem to be cool so far.  So...starting with what
        # should be the last character in the sentence (caretOffset - 2),
        # work our way to the beginning of the sentence, stopping when
        # we hit another sentence delimiter.
        #
        sentenceEndOffset = text.caretOffset - 2
        sentenceStartOffset = sentenceEndOffset

        while sentenceStartOffset >= 0:
            [currentChar, startOffset, endOffset] = \
                text.getTextAtOffset(sentenceStartOffset,
                                     Atspi.TextBoundaryType.CHAR)
            [previousChar, startOffset, endOffset] = \
                text.getTextAtOffset(sentenceStartOffset-1,
                                     Atspi.TextBoundaryType.CHAR)
            if self.utilities.isSentenceDelimiter(currentChar, previousChar):
                break
            else:
                sentenceStartOffset -= 1

        # If we came across a sentence delimiter before hitting any
        # text, we really don't have a previous sentence.
        #
        # Otherwise, get the sentence.  Remember we stopped when we
        # hit a sentence delimiter, so the sentence really starts at
        # sentenceStartOffset + 1.  getText also does not include
        # the character at sentenceEndOffset, so we need to adjust
        # for that, too.
        #
        if sentenceStartOffset == sentenceEndOffset:
            return False
        else:
            sentence = self.utilities.substring(obj, sentenceStartOffset + 1,
                                         sentenceEndOffset + 1)

        voice = self.speechGenerator.voice(obj=obj, string=sentence)
        sentence = self.utilities.adjustForRepeats(sentence)
        self.speakMessage(sentence, voice)
        return True

    def echoPreviousWord(self, obj, offset=None):
        """Speaks the word prior to the caret, as long as there is
        a word prior to the caret and there is no intervening word
        delimiter between the caret and the end of the word.

        The entry condition for this method is that the character
        prior to the current caret position is a word delimiter,
        and it's what caused this method to be called in the first
        place.

        Arguments:
        - obj: an Accessible object that implements the AccessibleText
               interface.
        - offset: if not None, the offset within the text to use as the
                  end of the word.
        """

        try:
            text = obj.queryText()
        except NotImplementedError:
            return False

        if not offset:
            if text.caretOffset == -1:
                offset = text.characterCount
            else:
                offset = text.caretOffset - 1

        if (offset < 0):
            return False

        [char, startOffset, endOffset] = \
            text.getTextAtOffset( \
                offset,
                Atspi.TextBoundaryType.CHAR)
        if not self.utilities.isWordDelimiter(char):
            return False

        # OK - we seem to be cool so far.  So...starting with what
        # should be the last character in the word (caretOffset - 2),
        # work our way to the beginning of the word, stopping when
        # we hit another word delimiter.
        #
        wordEndOffset = offset - 1
        wordStartOffset = wordEndOffset

        while wordStartOffset >= 0:
            [char, startOffset, endOffset] = \
                text.getTextAtOffset( \
                    wordStartOffset,
                    Atspi.TextBoundaryType.CHAR)
            if self.utilities.isWordDelimiter(char):
                break
            else:
                wordStartOffset -= 1

        # If we came across a word delimiter before hitting any
        # text, we really don't have a previous word.
        #
        # Otherwise, get the word.  Remember we stopped when we
        # hit a word delimiter, so the word really starts at
        # wordStartOffset + 1.  getText also does not include
        # the character at wordEndOffset, so we need to adjust
        # for that, too.
        #
        if wordStartOffset == wordEndOffset:
            return False
        else:
            word = self.utilities.\
                substring(obj, wordStartOffset + 1, wordEndOffset + 1)

        voice = self.speechGenerator.voice(obj=obj, string=word)
        word = self.utilities.adjustForRepeats(word)
        self.speakMessage(word, voice)
        return True

    def sayCharacter(self, obj):
        """Speak the character at the caret.

        Arguments:
        - obj: an Accessible object that implements the AccessibleText
               interface
        """

        text = obj.queryText()
        offset = text.caretOffset

        # If we have selected text and the last event was a move to the
        # right, then speak the character to the left of where the text
        # caret is (i.e. the selected character).
        #
        eventString, mods = self.utilities.lastKeyAndModifiers()
        if (mods & keybindings.SHIFT_MODIFIER_MASK) \
           and eventString in ["Right", "Down"]:
            offset -= 1

        character, startOffset, endOffset = text.getTextAtOffset(
            offset, Atspi.TextBoundaryType.CHAR)
        cthulhu.emitRegionChanged(obj, startOffset, endOffset, cthulhu.CARET_TRACKING)

        if not character or character == '\r':
            character = "\n"

        speakBlankLines = _settingsManager.getSetting('speakBlankLines')
        if character == "\n":
            line = text.getTextAtOffset(max(0, offset),
                                        Atspi.TextBoundaryType.LINE_START)
            if not line[0] or line[0] == "\n":
                # This is a blank line. Announce it if the user requested
                # that blank lines be spoken.
                if speakBlankLines:
                    self.speakMessage(messages.BLANK, interrupt=False)
                return

        if character in ["\n", "\r\n"]:
            # This is a blank line. Announce it if the user requested
            # that blank lines be spoken.
            if speakBlankLines:
                self.speakMessage(messages.BLANK, interrupt=False)
            return
        else:
            self.speakMisspelledIndicator(obj, offset)
            self.speakCharacter(character)

        self.pointOfReference["lastTextUnitSpoken"] = "char"

    def sayLine(self, obj):
        """Speaks the line of an AccessibleText object that contains the
        caret, unless the line is empty in which case it's ignored.

        Arguments:
        - obj: an Accessible object that implements the AccessibleText
               interface
        """

        [line, caretOffset, startOffset] = self.getTextLineAtCaret(obj)
        if len(line) and line != "\n":
            indentationDescription = self.utilities.indentationDescription(line)
            if indentationDescription:
                self.speakMessage(indentationDescription)

            endOffset = startOffset + len(line)
            cthulhu.emitRegionChanged(obj, startOffset, endOffset, cthulhu.CARET_TRACKING)

            utterance = []
            split = self.utilities.splitSubstringByLanguage(obj, startOffset, endOffset)
            for start, end, string, language, dialect in split:
                if not string:
                    continue

                voice = self.speechGenerator.voice(
                    obj=obj, string=string, language=language, dialect=dialect)
                string = self.utilities.adjustForLinks(obj, string, start)
                string = self.utilities.adjustForRepeats(string)
                if self.utilities.shouldVerbalizeAllPunctuation(obj):
                    string = self.utilities.verbalizeAllPunctuation(string)

                # Some synthesizers will verbalize the whitespace, so if we've already
                # described it, prevent double-presentation by stripping it off.
                if not utterance and indentationDescription:
                    string = string.lstrip()

                result = [string]
                result.extend(voice)
                utterance.append(result)
            speech.speak(utterance)
        else:
            # Speak blank line if appropriate.
            #
            self.sayCharacter(obj)

        self.pointOfReference["lastTextUnitSpoken"] = "line"

    def sayPhrase(self, obj, startOffset, endOffset):
        """Speaks the text of an Accessible object between the start and
        end offsets, unless the phrase is empty in which case it's ignored.

        Arguments:
        - obj: an Accessible object that implements the AccessibleText
               interface
        - startOffset: the start text offset.
        - endOffset: the end text offset.
        """

        phrase = self.utilities.expandEOCs(obj, startOffset, endOffset)
        if not phrase:
            return

        if len(phrase) > 1 or phrase.isalnum():
            result = self.utilities.indentationDescription(phrase)
            if result:
                self.speakMessage(result)

            cthulhu.emitRegionChanged(obj, startOffset, endOffset, cthulhu.CARET_TRACKING)

            voice = self.speechGenerator.voice(obj=obj, string=phrase)
            phrase = self.utilities.adjustForRepeats(phrase)
            if self.utilities.shouldVerbalizeAllPunctuation(obj):
                phrase = self.utilities.verbalizeAllPunctuation(phrase)

            utterance = [phrase]
            utterance.extend(voice)
            speech.speak(utterance)
        else:
            self.speakCharacter(phrase)

        self.pointOfReference["lastTextUnitSpoken"] = "phrase"

    def sayWord(self, obj):
        """Speaks the word at the caret, taking into account the previous caret position."""

        try:
            text = obj.queryText()
            offset = text.caretOffset
        except Exception:
            self.sayCharacter(obj)
            return

        word, startOffset, endOffset = \
            self.utilities.getWordAtOffsetAdjustedForNavigation(obj, offset)

        # Announce when we cross a hard line boundary.
        if "\n" in word:
            if _settingsManager.getSetting('enableSpeechIndentation'):
                self.speakCharacter("\n")
            if word.startswith("\n"):
                startOffset += 1
            elif word.endswith("\n"):
                endOffset -= 1
            word = text.getText(startOffset, endOffset)

        # sayPhrase 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:
            startOffset += matches[0].start()
            endOffset -= len(word) - matches[-1].end()
            word = text.getText(startOffset, endOffset)

        string = word.replace("\n", "\\n")
        msg = (
            f"DEFAULT: Final word at offset {offset} is '{string}' "
            f"({startOffset}-{endOffset})"
        )
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        self.speakMisspelledIndicator(obj, startOffset)
        self.sayPhrase(obj, startOffset, endOffset)
        self.pointOfReference["lastTextUnitSpoken"] = "word"

    def presentObject(self, obj, **args):
        interrupt = args.get("interrupt", False)
        tokens = ["DEFAULT: Presenting object", obj, ". Interrupt:", interrupt]
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        if not args.get("speechonly", False):
            self.updateBraille(obj, **args)
        utterances = self.speechGenerator.generateSpeech(obj, **args)
        speech.speak(utterances, interrupt=interrupt)

    def stopSpeechOnActiveDescendantChanged(self, event):
        """Whether or not speech should be stopped prior to setting the
        locusOfFocus in onActiveDescendantChanged.

        Arguments:
        - event: the Event

        Returns True if speech should be stopped; False otherwise.
        """

        if not event.any_data:
            return True

        # In an object which manages its descendants, the
        # 'descendants' may really be a single object which changes
        # its name. If the name-change occurs followed by the active
        # descendant changing (to the same object) we won't present
        # the locusOfFocus because it hasn't changed. Thus we need to
        # be sure not to cut of the presentation of the name-change
        # event.

        if cthulhu_state.locusOfFocus == event.any_data:
            names = self.pointOfReference.get('names', {})
            oldName = names.get(hash(cthulhu_state.locusOfFocus), '')
            if not oldName or AXObject.get_name(event.any_data) == oldName:
                return False

        if event.source == cthulhu_state.locusOfFocus == AXObject.get_parent(event.any_data):
            return False

        return True

    def getFlatReviewContext(self):
        """Returns the flat review context, creating one if necessary."""

        return self.flatReviewPresenter.get_or_create_context(self)

    def updateBrailleReview(self, targetCursorCell=0):
        """Obtains the braille regions for the current flat review line
        and displays them on the braille display.  If the targetCursorCell
        is non-0, then an attempt will be made to position the review cursor
        at that cell.  Otherwise, we will pan in display-sized increments
        to show the review cursor."""

        if not _settingsManager.getSetting('enableBraille') \
           and not _settingsManager.getSetting('enableBrailleMonitor'):
            debug.printMessage(debug.LEVEL_INFO, "BRAILLE: update review disabled", True)
            return

        [regions, regionWithFocus] = self.flatReviewPresenter.get_braille_regions(self)
        if not regions:
            regions = []
            regionWithFocus = None

        line = self.getNewBrailleLine()
        self.addBrailleRegionsToLine(regions, line)
        braille.setLines([line])
        self.setBrailleFocus(regionWithFocus, False)
        if regionWithFocus and not targetCursorCell:
            offset = regionWithFocus.brailleOffset + regionWithFocus.cursorOffset
            tokens = ["DEFAULT: Update to", offset, "in", regionWithFocus]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            self.panBrailleToOffset(offset)

        if self.justEnteredFlatReviewMode:
            self.refreshBraille(True, self.targetCursorCell)
            self.justEnteredFlatReviewMode = False
        else:
            self.refreshBraille(True, targetCursorCell)

    def _setFlatReviewContextToBeginningOfBrailleDisplay(self):
        """Sets the character of interest to be the first character showing
        at the beginning of the braille display."""

        # The first character on the flat review line has to be in object with text.
        def isTextOrComponent(x):
            return isinstance(x, (braille.ReviewText, braille.ReviewComponent))

        regions = self.flatReviewPresenter.get_braille_regions(self)[0]
        regions = list(filter(isTextOrComponent, regions))
        tokens = ["DEFAULT: Text/Component regions on line:"]
        for region in regions:
            tokens.extend(["\n", region])
        debug.printTokens(debug.LEVEL_INFO, tokens, True)

        # TODO - JD: The current code was stopping on the first region which met the
        # following condition. Is that definitely the right thing to do? Assume so for now.
        # Also: Should the default script be accessing things like the viewport directly??
        def isMatch(x):
            return x is not None and x.brailleOffset + len(x.string) > braille.viewport[0]

        regions = list(filter(isMatch, regions))
        if not regions:
            msg = "DEFAULT: Could not find review region to move to start of display"
            debug.printMessage(debug.LEVEL_INFO, msg, True)
            return

        tokens = ["DEFAULT: Candidates for start of display:"]
        for region in regions:
            tokens.extend(["\n", region])
        debug.printTokens(debug.LEVEL_INFO, tokens, True)


        # TODO - JD: Again, for now we're preserving the original behavior of choosing the first.
        region = regions[0]
        position = max(region.brailleOffset, braille.viewport[0])
        if region.contracted:
            offset = region.inPos[position - region.brailleOffset]
        else:
            offset = position - region.brailleOffset
        if isinstance(region.zone, flat_review.TextZone):
            offset += region.zone.startOffset
        msg = f"DEFAULT: Offset for region: {offset}"
        debug.printMessage(debug.LEVEL_INFO, msg, True)

        [word, charOffset] = region.zone.getWordAtOffset(offset)
        if word:
            tokens = ["DEFAULT: Setting start of display to", word, ", ", charOffset]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            context = self.getFlatReviewContext()
            context.setCurrent(
                word.zone.line.index,
                word.zone.index,
                word.index,
                charOffset)
        else:
            tokens = ["DEFAULT: Setting start of display to", region.zone]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            context = self.getFlatReviewContext()
            context.setCurrent(
                region.zone.line.index,
                region.zone.index,
                0, # word index
                0) # character index

    def find(self, query=None):
        """Searches for the specified query.  If no query is specified,
        it searches for the query specified in the Cthulhu Find dialog.

        Arguments:
        - query: The search query to find.
        """

        if not query:
            query = find.getLastQuery()
        if query:
            context = self.getFlatReviewContext()
            location = query.findQuery(context)
            if not location:
                self.presentMessage(messages.STRING_NOT_FOUND)
            else:
                context.setCurrent(location.lineIndex, location.zoneIndex, \
                                   location.wordIndex, location.charIndex)
                self.flatReviewPresenter.present_item(self)
                self.targetCursorCell = self.getBrailleCursorCell()

    def textLines(self, obj, offset=None):
        """Creates a generator that can be used to iterate over each line
        of a text object, starting at the caret offset.

        Arguments:
        - obj: an Accessible that has a text specialization

        Returns an iterator that produces elements of the form:
        [SayAllContext, acss], where SayAllContext has the text to be
        spoken and acss is an ACSS instance for speaking the text.
        """

        self._sayAllIsInterrupted = False
        try:
            text = obj.queryText()
        except Exception:
            self._inSayAll = False
            self._sayAllContexts = []
            return

        self._inSayAll = True
        length = text.characterCount
        if offset is None:
            offset = text.caretOffset

        # Determine the correct "say all by" mode to use.
        #
        sayAllStyle = _settingsManager.getSetting('sayAllStyle')
        if sayAllStyle == settings.SAYALL_STYLE_SENTENCE:
            mode = Atspi.TextBoundaryType.SENTENCE_START
        elif sayAllStyle == settings.SAYALL_STYLE_LINE:
            mode = Atspi.TextBoundaryType.LINE_START
        else:
            mode = Atspi.TextBoundaryType.LINE_START

        priorObj = obj

        # Get the next line of text to read
        #
        done = False
        while not done:
            speech.speak(self.speechGenerator.generateContext(obj, priorObj=priorObj))

            lastEndOffset = -1
            while offset < length:
                [lineString, startOffset, endOffset] = text.getTextAtOffset(
                    offset, mode)

                # Some applications that don't support sentence boundaries
                # will provide the line boundary results instead; others
                # will return nothing.
                #
                if not lineString:
                    mode = Atspi.TextBoundaryType.LINE_START
                    [lineString, startOffset, endOffset] = \
                        text.getTextAtOffset(offset, mode)

                if endOffset > text.characterCount:
                    tokens = ["DEFAULT: end offset", endOffset, " > character count",
                              text.characterCount,
                              "resulting from text.getTextAtOffset(", offset, mode, ") for", obj]
                    debug.printTokens(debug.LEVEL_INFO, tokens, True)
                    endOffset = text.characterCount

                # [[[WDW - HACK: this is here because getTextAtOffset
                # tends not to be implemented consistently across toolkits.
                # Sometimes it behaves properly (i.e., giving us an endOffset
                # that is the beginning of the next line), sometimes it
                # doesn't (e.g., giving us an endOffset that is the end of
                # the current line).  So...we hack.  The whole 'max' deal
                # is to account for lines that might be a brazillion lines
                # long.]]]
                #
                if endOffset == lastEndOffset:
                    offset = max(offset + 1, lastEndOffset + 1)
                    lastEndOffset = endOffset
                    continue

                lastEndOffset = endOffset
                offset = endOffset

                voice = self.speechGenerator.voice(obj=obj, string=lineString)
                if voice and isinstance(voice, list):
                    voice = voice[0]

                lineString = \
                    self.utilities.adjustForLinks(obj, lineString, startOffset)
                lineString = self.utilities.adjustForRepeats(lineString)

                context = speechserver.SayAllContext(
                    obj, lineString, startOffset, endOffset)
                tokens = ["DEFAULT:", context]
                debug.printTokens(debug.LEVEL_INFO, tokens, True)
                self._sayAllContexts.append(context)
                self.eventSynthesizer.scroll_into_view(obj, startOffset, endOffset)
                yield [context, voice]

            moreLines = False
            relation = AXObject.get_relation(obj, Atspi.RelationType.FLOWS_TO)
            if relation:
                priorObj = obj
                obj = relation.getTarget(0)

                try:
                    text = obj.queryText()
                except NotImplementedError:
                    return

                length = text.characterCount
                offset = 0
                moreLines = True
                break
            if not moreLines:
                done = True

        self._inSayAll = False
        self._sayAllContexts = []

        msg = "DEFAULT: textLines complete. Verifying SayAll status"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self.inSayAll()

    def getTextLineAtCaret(self, obj, offset=None, startOffset=None, endOffset=None):
        """To-be-removed. Returns the string, caretOffset, startOffset."""

        try:
            text = obj.queryText()
            offset = text.caretOffset
            characterCount = text.characterCount
        except NotImplementedError:
            return ["", 0, 0]
        except Exception:
            tokens = ["DEFAULT: Exception getting offset and length for", obj]
            debug.printTokens(debug.LEVEL_INFO, tokens, True)
            return ["", 0, 0]

        if characterCount == 0:
            return ["", 0, 0]

        targetOffset = startOffset
        if targetOffset is None:
            targetOffset = max(0, offset)

        # The offset might be positioned at the very end of the text area.
        # In these cases, calling text.getTextAtOffset on an offset that's
        # not positioned to a character can yield unexpected results.  In
        # particular, we'll see the Gecko toolkit return a start and end
        # offset of (0, 0), and we'll see other implementations, such as
        # gedit, return reasonable results (i.e., gedit will give us the
        # last line).
        #
        # In order to accommodate the differing behavior of different
        # AT-SPI implementations, we'll make sure we give getTextAtOffset
        # the offset of an actual character.  Then, we'll do a little check
        # to see if that character is a newline - if it is, we'll treat it
        # as the line.
        #
        if targetOffset == characterCount:
            fixedTargetOffset = max(0, targetOffset - 1)
            character = text.getText(fixedTargetOffset, fixedTargetOffset + 1)
        else:
            fixedTargetOffset = targetOffset
            character = None

        if (targetOffset == characterCount) \
            and (character == "\n"):
            lineString = ""
            startOffset = fixedTargetOffset
        else:
            # Get the line containing the caret.  [[[TODO: HACK WDW - If
            # there's only 1 character in the string, well, we get it.  We
            # do this because Gecko's implementation of getTextAtOffset
            # is broken if there is just one character in the string.]]]
            #
            if (characterCount == 1):
                lineString = text.getText(fixedTargetOffset, fixedTargetOffset + 1)
                startOffset = fixedTargetOffset
            else:
                if fixedTargetOffset == -1:
                    fixedTargetOffset = characterCount
                try:
                    [lineString, startOffset, endOffset] = text.getTextAtOffset(
                        fixedTargetOffset, Atspi.TextBoundaryType.LINE_START)
                except Exception:
                    return ["", 0, 0]

            # Sometimes we get the trailing line-feed-- remove it
            # It is important that these are in order.
            # In some circumstances we might get:
            # word word\r\n
            # so remove \n, and then remove \r.
            # See bgo#619332.
            #
            lineString = lineString.rstrip('\n')
            lineString = lineString.rstrip('\r')

        return [lineString, text.caretOffset, startOffset]

    def phoneticSpellCurrentItem(self, itemString):
        """Phonetically spell the current flat review word or line.

        Arguments:
        - itemString: the string to phonetically spell.
        """

        for (charIndex, character) in enumerate(itemString):
            voice = self.speechGenerator.voice(string=character)
            phoneticString = phonnames.getPhoneticName(character.lower())
            self.speakMessage(phoneticString, voice)

    def _saveLastCursorPosition(self, obj, caretOffset):
        """Save away the current text cursor position for next time.

        Arguments:
        - obj: the current accessible
        - caretOffset: the cursor position within this object
        """

        prevObj, prevOffset = self.pointOfReference.get("lastCursorPosition", (None, -1))
        self.pointOfReference["penultimateCursorPosition"] = prevObj, prevOffset
        self.pointOfReference["lastCursorPosition"] = obj, caretOffset

    def systemBeep(self):
        """Rings the system bell. This is really a hack. Ideally, we want
        a method that will present an earcon (any sound designated for the
        purpose of representing an error, event etc)
        """

        print("\a")

    def speakMisspelledIndicator(self, obj, offset):
        """Speaks an announcement indicating that a given word is misspelled.

        Arguments:
        - obj: An accessible which implements the accessible text interface.
        - offset: Offset in the accessible's text for which to retrieve the
          attributes.
        """

        if _settingsManager.getSetting('speakMisspelledIndicator'):
            try:
                text = obj.queryText()
            except Exception:
                return
            # If we're on whitespace, we cannot be on a misspelled word.
            #
            charAndOffsets = \
                text.getTextAtOffset(offset, Atspi.TextBoundaryType.CHAR)
            if not charAndOffsets[0].strip() \
               or self.utilities.isWordDelimiter(charAndOffsets[0]):
                self._lastWordCheckedForSpelling = charAndOffsets[0]
                return

            wordAndOffsets = \
                text.getTextAtOffset(offset, Atspi.TextBoundaryType.WORD_START)
            if self.utilities.isWordMisspelled(obj, offset) \
               and wordAndOffsets[0] != self._lastWordCheckedForSpelling:
                self.speakMessage(messages.MISSPELLED)
            # Store this word so that we do not continue to present the
            # presence of the red squiggly as the user arrows amongst
            # the characters.
            #
            self._lastWordCheckedForSpelling = wordAndOffsets[0]

    ############################################################################
    #                                                                          #
    # Presentation methods                                                     #
    # (scripts should not call methods in braille.py or speech.py directly)    #
    #                                                                          #
    ############################################################################

    def presentationInterrupt(self):
        """Convenience method to interrupt presentation of whatever is being
        presented at the moment."""

        msg = "DEFAULT: Interrupting presentation"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        speech.stop()
        braille.killFlash()

    def presentKeyboardEvent(self, event):
        """Convenience method to present the KeyboardEvent event. Returns True
        if we fully present the event; False otherwise."""

        if not event.isPressedKey():
            self._sayAllIsInterrupted = False
            self.utilities.clearCachedCommandState()

        if not event.shouldEcho or event.isCthulhuModified():
            return False

        role = AXObject.get_role(cthulhu_state.locusOfFocus)
        if role in [Atspi.Role.DIALOG, Atspi.Role.FRAME, Atspi.Role.WINDOW]:
            focusedObject = AXUtilities.get_focused_object(cthulhu_state.activeWindow)
            if focusedObject:
                cthulhu.setLocusOfFocus(None, focusedObject, False)
                role = AXObject.get_role(focusedObject)

        if role == Atspi.Role.PASSWORD_TEXT and not event.isLockingKey():
            return False

        if not event.isPressedKey():
            return False

        braille.displayKeyEvent(event)
        cthulhuModifierPressed = event.isCthulhuModifier() and event.isPressedKey()
        if event.isCharacterEchoable() and not cthulhuModifierPressed:
            return False

        msg = "DEFAULT: Presenting keyboard event"
        debug.printMessage(debug.LEVEL_INFO, msg, True)
        self.speakKeyEvent(event)
        return True

    def presentMessage(self, fullMessage, briefMessage=None, voice=None, resetStyles=True,
                       force=False):
        """Convenience method to speak a message and 'flash' it in braille.

        Arguments:
        - fullMessage: This can be a string or a list. This will be presented
          as the message for users whose flash or message verbosity level is
          verbose.
        - briefMessage: This can be a string or a list. This will be presented
          as the message for users whose flash or message verbosity level is
          brief. Note that providing no briefMessage will result in the full
          message being used for either. Callers wishing to present nothing as
          the briefMessage should set briefMessage to an empty string.
        - voice: The voice to use when speaking this message. By default, the
          "system" voice will be used.
        """

        if not fullMessage:
            return

        if briefMessage is None:
            briefMessage = fullMessage

        if _settingsManager.getSetting('enableSpeech'):
            if not _settingsManager.getSetting('messagesAreDetailed'):
                message = briefMessage
            else:
                message = fullMessage
            if message:
                self.speakMessage(message, voice=voice, resetStyles=resetStyles, force=force)

        if (_settingsManager.getSetting('enableBraille') \
             or _settingsManager.getSetting('enableBrailleMonitor')) \
           and _settingsManager.getSetting('enableFlashMessages'):
            if not _settingsManager.getSetting('flashIsDetailed'):
                message = briefMessage
            else:
                message = fullMessage
            if not message:
                return

            if isinstance(message[0], list):
                message = message[0]
            if isinstance(message, list):
                message = [i for i in message if isinstance(i, str)]
                message = " ".join(message)

            if _settingsManager.getSetting('flashIsPersistent'):
                duration = -1
            else:
                duration = _settingsManager.getSetting('brailleFlashTime')

            braille.displayMessage(message, flashTime=duration)

    def idleMessage(self):
        """Convenience method to tell speech and braille engines to hand off
        control to other screen readers."""

        braille.disableBraille()

    @staticmethod
    def __play(sounds, interrupt=True):
        if not sounds:
            return

        if not isinstance(sounds, list):
            sounds = [sounds]

        _player = sound.getPlayer()
        _player.play(sounds[0], interrupt)
        for i in range(1, len(sounds)):
            sound.play(sounds[i], interrupt=False)

    @staticmethod
    def addBrailleRegionToLine(region, line):
        """Adds the braille region to the line.

        Arguments:
        - region: a braille.Region (e.g. what is returned by the braille
          generator's generateBraille() method.
        - line: a braille.Line
        """

        line.addRegion(region)

    @staticmethod
    def addBrailleRegionsToLine(regions, line):
        """Adds the braille region to the line.

        Arguments:
        - regions: a series of braille.Region instances (a single instance
          being what is returned by the braille generator's generateBraille()
          method.
        - line: a braille.Line
        """

        line.addRegions(regions)

    @staticmethod
    def addToLineAsBrailleRegion(string, line):
        """Creates a Braille Region out of string and adds it to the line.

        Arguments:
        - string: the string to be displayed
        - line: a braille.Line
        """

        line.addRegion(braille.Region(string))

    @staticmethod
    def brailleRegionsFromStrings(strings):
        """Creates a list of braille regions from the list of strings.

        Arguments:
        - strings: a list of strings from which to create the list of
          braille Region instances

        Returns the list of braille Region instances
        """

        brailleRegions = []
        for string in strings:
            brailleRegions.append(braille.Region(string))

        return brailleRegions

    @staticmethod
    def clearBraille():
        """Clears the logical structure, but keeps the Braille display as is
        (until a refresh operation)."""

        braille.clear()

    @staticmethod
    def displayBrailleMessage(message, cursor=-1, flashTime=0):
        """Displays a single line, setting the cursor to the given position,
        ensuring that the cursor is in view.

        Arguments:
        - message: the string to display
        - cursor: the 0-based cursor position, where -1 (default) means no
          cursor
        - flashTime:  if non-0, the number of milliseconds to display the
          regions before reverting back to what was there before. A 0 means
          to not do any flashing.  A negative number means to display the
          message until some other message comes along or the user presses
          a cursor routing key.
        """

        if not _settingsManager.getSetting('enableBraille') \
           and not _settingsManager.getSetting('enableBrailleMonitor'):
            debug.printMessage(debug.LEVEL_INFO, "BRAILLE: display message disabled", True)
            return

        braille.displayMessage(message, cursor, flashTime)

    @staticmethod
    def displayBrailleRegions(regionInfo, flashTime=0):
        """Displays a list of regions on a single line, setting focus to the
        specified region.  The regionInfo parameter is something that is
        typically returned by a call to braille_generator.generateBraille.

        Arguments:
        - regionInfo: a list where the first element is a list of regions
          to display and the second element is the region with focus (must
          be in the list from element 0)
        - flashTime:  if non-0, the number of milliseconds to display the
          regions before reverting back to what was there before. A 0 means
          to not do any flashing. A negative number means to display the
          message until some other message comes along or the user presses
          a cursor routing key.
        """

        if not _settingsManager.getSetting('enableBraille') \
           and not _settingsManager.getSetting('enableBrailleMonitor'):
            debug.printMessage(debug.LEVEL_INFO, "BRAILLE: display regions disabled", True)
            return

        braille.displayRegions(regionInfo, flashTime)

    def displayBrailleForObject(self, obj):
        """Convenience method for scripts combining the call to the braille
        generator for the script with the call to displayBrailleRegions.

        Arguments:
        - obj: the accessible object to display in braille
        """

        regions = self.brailleGenerator.generateBraille(obj)
        self.displayBrailleRegions(regions)

    @staticmethod
    def getBrailleCaretContext(event):
        """Gets the accesible and caret offset associated with the given
        event.  The event should have a BrlAPI event that contains an
        argument value that corresponds to a cell on the display.

        Arguments:
        - event: an instance of input_event.BrailleEvent.  event.event is
          the dictionary form of the expanded BrlAPI event.
        """

        return braille.getCaretContext(event)

    @staticmethod
    def getBrailleCursorCell():
        """Returns the value of position of the braille cell which has the
        cursor. A value of 0 means no cell has the cursor."""

        return braille.cursorCell

    @staticmethod
    def getNewBrailleLine(clearBraille=False, addLine=False):
        """Creates a new braille Line.

        Arguments:
        - clearBraille: Whether the display should be cleared.
        - addLine: Whether the line should be added to the logical display
          for painting.

        Returns the new Line.
        """

        if clearBraille:
            braille.clear()
        line = braille.Line()
        if addLine:
            braille.addLine(line)

        return line

    @staticmethod
    def getNewBrailleComponent(accessible, string, cursorOffset=0,
                               indicator='', expandOnCursor=False):
        """Creates a new braille Component.

        Arguments:
        - accessible: the accessible associated with this region
        - string: the string to be displayed
        - cursorOffset: a 0-based index saying where to draw the cursor
          for this Region if it gets focus

        Returns the new Component.
        """

        return braille.Component(accessible, string, cursorOffset,
                                 indicator, expandOnCursor)

    @staticmethod
    def getNewBrailleRegion(string, cursorOffset=0, expandOnCursor=False):
        """Creates a new braille Region.

        Arguments:
        - string: the string to be displayed
        - cursorOffset: a 0-based index saying where to draw the cursor
          for this Region if it gets focus

        Returns the new Region.
        """

        return braille.Region(string, cursorOffset, expandOnCursor)

    @staticmethod
    def getNewBrailleText(accessible, label="", eol="", startOffset=None,
                          endOffset=None):

        """Creates a new braille Text region.

        Arguments:
        - accessible: the accessible associated with this region and which
          implements AtkText
        - label: an optional label to display
        - eol: the endOfLine indicator

        Returns the new Text region.
        """

        return braille.Text(accessible, label, eol, startOffset, endOffset)

    @staticmethod
    def isBrailleBeginningShowing():
        """If True, the beginning of the line is showing on the braille
        display."""

        return braille.beginningIsShowing

    @staticmethod
    def isBrailleEndShowing():
        """If True, the end of the line is showing on the braille display."""

        return braille.endIsShowing

    @staticmethod
    def panBrailleInDirection(panAmount=0, panToLeft=True):
        """Pans the display to the left, limiting the pan to the beginning
        of the line being displayed.

        Arguments:
        - panAmount: the amount to pan.  A value of 0 means the entire
          width of the physical display.
        - panToLeft: if True, pan to the left; otherwise to the right

        Returns True if a pan actually happened.
        """

        if panToLeft:
            return braille.panLeft(panAmount)
        else:
            return braille.panRight(panAmount)

    @staticmethod
    def panBrailleToOffset(offset):
        """Automatically pan left or right to make sure the current offset
        is showing."""

        braille.panToOffset(offset)

    @staticmethod
    def presentItemsInBraille(items):
        """Method to braille a list of items. Scripts should call this
        method rather than handling the creation and displaying of a
        braille line directly.

        Arguments:
        - items: a list of strings to be presented
        """

        line = braille.getShowingLine()
        for item in items:
            line.addRegion(braille.Region(" " + item))

        braille.refresh()

    def updateBrailleForNewCaretPosition(self, obj):
        """Try to reposition the cursor without having to do a full update."""

        if not _settingsManager.getSetting('enableBraille') \
           and not _settingsManager.getSetting('enableBrailleMonitor'):
            debug.printMessage(debug.LEVEL_INFO, "BRAILLE: update caret disabled", True)
            return

        brailleNeedsRepainting = True
        line = braille.getShowingLine()
        for region in line.regions:
            if isinstance(region, braille.Text) and region.accessible == obj:
                if region.repositionCursor():
                    self.refreshBraille(True)
                    brailleNeedsRepainting = False
                break

        if brailleNeedsRepainting:
            self.updateBraille(obj)

    @staticmethod
    def refreshBraille(panToCursor=True, targetCursorCell=0, getLinkMask=True,
                       stopFlash=True):
        """This is the method scripts should use to refresh braille rather
        than calling self.refreshBraille() directly. The intent is to centralize
        such calls into as few places as possible so that we can easily and
        safely not perform braille-related functions for users who do not
        have braille and/or the braille monitor enabled.

        Arguments:

        - panToCursor: if True, will adjust the viewport so the cursor is
          showing.
        - targetCursorCell: Only effective if panToCursor is True.
          0 means automatically place the cursor somewhere on the display so
          as to minimize movement but show as much of the line as possible.
          A positive value is a 1-based target cell from the left side of
          the display and a negative value is a 1-based target cell from the
          right side of the display.
        - getLinkMask: Whether or not we should take the time to get the
          attributeMask for links. Reasons we might not want to include
          knowing that we will fail and/or it taking an unreasonable
          amount of time (AKA Gecko).
        - stopFlash: if True, kill any flashed message that may be showing.
        """

        braille.refresh(panToCursor, targetCursorCell, getLinkMask, stopFlash)

    @staticmethod
    def setBrailleFocus(region, panToFocus=True, getLinkMask=True):
        """Specififes the region with focus.  This region will be positioned
        at the home position if panToFocus is True.

        Arguments:
        - region: the given region, which much be in a line that has been
          added to the logical display
        - panToFocus: whether or not to position the region at the home
          position
        - getLinkMask: Whether or not we should take the time to get the
          attributeMask for links. Reasons we might not want to include
          knowing that we will fail and/or it taking an unreasonable
          amount of time (AKA Gecko).
        """

        braille.setFocus(region, panToFocus, getLinkMask)

    @staticmethod
    def _setContractedBraille(event):
        """Turns contracted braille on or off based upon the event.

        Arguments:
        - event: an instance of input_event.BrailleEvent.  event.event is
          the dictionary form of the expanded BrlAPI event.
        """

        braille.setContractedBraille(event)

    ########################################################################
    #                                                                      #
    # Speech methods                                                       #
    # (scripts should not call methods in speech.py directly)              #
    #                                                                      #
    ########################################################################

    def speakKeyEvent(self, event):
        """Method to speak a keyboard event. Scripts should use this method
        rather than calling speech.speakKeyEvent directly."""

        string = None
        if event.isPrintableKey():
            string = event.event_string

        voice = self.speechGenerator.voice(string=string)
        speech.speakKeyEvent(event, voice)

    def speakCharacter(self, character):
        """Method to speak a single character. Scripts should use this
        method rather than calling speech.speakCharacter directly."""

        voice = self.speechGenerator.voice(string=character)
        speech.speakCharacter(character, voice)

    def speakMessage(self, string, voice=None, interrupt=True, resetStyles=True, force=False):
        """Method to speak a single string. Scripts should use this
        method rather than calling speech.speak directly.

        - string: The string to be spoken.
        - voice: The voice to use. By default, the "system" voice will
          be used.
        - interrupt: If True, any current speech should be interrupted
          prior to speaking the new text.
        """

        if not _settingsManager.getSetting('enableSpeech') \
           or (_settingsManager.getSetting('onlySpeakDisplayedText') and not force):
            return

        voices = _settingsManager.getSetting('voices')
        systemVoice = voices.get(settings.SYSTEM_VOICE)

        voice = voice or systemVoice
        if voice == systemVoice and resetStyles:
            capStyle = _settingsManager.getSetting('capitalizationStyle')
            _settingsManager.setSetting('capitalizationStyle', settings.CAPITALIZATION_STYLE_NONE)
            self.speechAndVerbosityManager.update_capitalization_style()

            punctStyle = _settingsManager.getSetting('verbalizePunctuationStyle')
            _settingsManager.setSetting('verbalizePunctuationStyle',
                                         settings.PUNCTUATION_STYLE_NONE)
            self.speechAndVerbosityManager.update_punctuation_level()

        speech.speak(string, voice, interrupt)

        if voice == systemVoice and resetStyles:
            _settingsManager.setSetting('capitalizationStyle', capStyle)
            self.speechAndVerbosityManager.update_capitalization_style()

            _settingsManager.setSetting('verbalizePunctuationStyle', punctStyle)
            self.speechAndVerbosityManager.update_punctuation_level()

    @staticmethod
    def presentItemsInSpeech(items):
        """Method to speak a list of items. Scripts should call this
        method rather than handling the creation and speaking of
        utterances directly.

        Arguments:
        - items: a list of strings to be presented
        """

        utterances = []
        for item in items:
            utterances.append(item)

        speech.speak(utterances)

    def speakUnicodeCharacter(self, character):
        """ Speaks some information about an unicode character.
        At the moment it just announces the character unicode number but
        this information may be changed in the future

        Arguments:
        - character: the character to speak information of
        """
        speech.speak(messages.UNICODE % \
                         self.utilities.unicodeValueString(character))
