# Orca
#
# Copyright 2004-2008 Sun Microsystems Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 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
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library 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) 2005-2008 Sun Microsystems Inc."
__license__   = "LGPL"

import locale
import math
import sys
import time

import pyatspi
import braille
import chnames
import debug
import find
import flat_review
import input_event
import keybindings
import mag
import outline
import orca
import orca_prefs
import orca_state
import phonnames
import pronunciation_dict
import punctuation_settings
import rolenames
import script
import settings
import speech
import speechserver
import mouse_review
import text_attribute_names

from orca_i18n import _         # for gettext support
from orca_i18n import ngettext  # for ngettext support
from orca_i18n import C_        # to provide qualified translatable strings

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

class Script(script.Script):

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

    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.flatReviewContext  = None
        self.windowActivateTime = None
        self.lastReviewCurrentEvent = None
        self.exitLearnModeKeyBinding = None
        self.targetCursorCell = None

        self.justEnteredFlatReviewMode = False

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

        # Used to determine whether the used double clicked on the
        # "where am I" key.
        #
        self.lastWhereAmIEvent = None

        # Used to determine whether the used double clicked on the
        # "say all" key.
        #
        self.lastSayAllEvent = None

        # Unicode currency symbols (populated by the
        # getUnicodeCurrencySymbols() routine).
        #
        self._unicodeCurrencySymbols = []

        # Used by the visualAppearanceChanged routine for updating whether
        # progress bars are spoken.
        #
        self.lastProgressBarTime = {}
        self.lastProgressBarValue = {}

        self.lastSelectedMenu = None

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

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

        self.inputEventHandlers["leftClickReviewItemHandler"] = \
            input_event.InputEventHandler(
                Script.leftClickReviewItem,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  A left click means to generate
                # a left mouse button click on the current item.
                #
                _("Performs left click on current flat review item."))

        self.inputEventHandlers["rightClickReviewItemHandler"] = \
             input_event.InputEventHandler(
                Script.rightClickReviewItem,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  A right click means to generate
                # a right mouse button click on the current item.
                #
                _("Performs right click on current flat review item."))

        self.inputEventHandlers["sayAllHandler"] = \
            input_event.InputEventHandler(
                Script.sayAll,
                # Translators: the Orca "SayAll" command allows the
                # user to press a key and have the entire document in
                # a window be automatically spoken to the user.  If
                # the user presses any key during a SayAll operation,
                # the speech will be interrupted and the cursor will
                # be positioned at the point where the speech was
                # interrupted.
                #
                _("Speaks entire document."))

        self.inputEventHandlers["whereAmIBasicHandler"] = \
            input_event.InputEventHandler(
                Script.whereAmIBasic,
                # Translators: the "Where am I" feature of Orca allows
                # a user to press a key and then have information
                # about their current context spoken and brailled to
                # them.  For example, the information may include the
                # name of the current pushbutton with focus as well as
                # its mnemonic.
                #
                _("Performs the basic where am I operation."))

        self.inputEventHandlers["whereAmIDetailedHandler"] = \
            input_event.InputEventHandler(
                Script.whereAmIDetailed,
                # Translators: the "Where am I" feature of Orca allows
                # a user to press a key and then have information
                # about their current context spoken and brailled to
                # them.  For example, the information may include the
                # name of the current pushbutton with focus as well as
                # its mnemonic.
                #
                _("Performs the detailed where am I operation."))

        self.inputEventHandlers["getTitleHandler"] = \
            input_event.InputEventHandler(
                Script.getTitle,
                # Translators: This command will cause the window's
                # title to be spoken.
                #
                _("Speaks the title bar."))

        self.inputEventHandlers["getStatusBarHandler"] = \
            input_event.InputEventHandler(
                Script.getStatusBar,
                # Translators: This command will cause the window's
                # status bar contents to be spoken.
                #
                _("Speaks the status bar."))

        self.inputEventHandlers["findHandler"] = \
            input_event.InputEventHandler(
                orca.showFindGUI,
                # Translators: the Orca "Find" dialog allows a user to
                # search for text in a window and then move focus to
                # that text.  For example, they may want to find the
                # "OK" button.
                #
                _("Opens the Orca Find dialog."))

        self.inputEventHandlers["findNextHandler"] = \
            input_event.InputEventHandler(
                Script.findNext,
                # Translators: the Orca "Find" dialog allows a user to
                # search for text in a window and then move focus to
                # that text.  For example, they may want to find the
                # "OK" button.  This string is used for finding the
                # next occurence of a string.
                #
                _("Searches for the next instance of a string."))

        self.inputEventHandlers["findPreviousHandler"] = \
            input_event.InputEventHandler(
                Script.findPrevious,
                # Translators: the Orca "Find" dialog allows a user to
                # search for text in a window and then move focus to
                # that text.  For example, they may want to find the
                # "OK" button.  This string is used for finding the
                # previous occurence of a string.
                #
                _("Searches for the previous instance of a string."))

        self.inputEventHandlers["showZonesHandler"] = \
            input_event.InputEventHandler(
                Script.showZones,
                # Translators: this is a debug message that Orca users
                # will not normally see. It describes a debug routine that
                # paints rectangles around the interesting (e.g., text)
                # zones in the active window for the application that
                # currently has focus.
                #
                _("Paints and prints the visible zones in the active window."))

        self.inputEventHandlers["toggleFlatReviewModeHandler"] = \
            input_event.InputEventHandler(
                Script.toggleFlatReviewMode,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.
                #
                _("Enters and exits flat review mode."))

        self.inputEventHandlers["reviewPreviousLineHandler"] = \
            input_event.InputEventHandler(
                Script.reviewPreviousLine,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.
                #
                _("Moves flat review to the beginning of the previous line."))

        self.inputEventHandlers["reviewHomeHandler"] = \
            input_event.InputEventHandler(
                Script.reviewHome,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  The home position is the
                # beginning of the content in the window.
                #
                _("Moves flat review to the home position."))

        self.inputEventHandlers["reviewCurrentLineHandler"] = \
            input_event.InputEventHandler(
                Script.reviewCurrentLine,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  This particular command will
                # cause Orca to speak the current line.
                #
                _("Speaks the current flat review line."))

        self.inputEventHandlers["reviewSpellCurrentLineHandler"] = \
            input_event.InputEventHandler(
                Script.reviewSpellCurrentLine,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}. This particular command will
                # cause Orca to spell the current line.
                #
                _("Spells the current flat review line."))

        self.inputEventHandlers["reviewPhoneticCurrentLineHandler"] = \
            input_event.InputEventHandler(
                Script.reviewPhoneticCurrentLine,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}. This particular command will
                # cause Orca to "phonetically spell" the current line,
                # saying "Alpha" for "a", "Bravo" for "b" and so on.
                #
                _("Phonetically spells the current flat review line."))

        self.inputEventHandlers["reviewNextLineHandler"] = \
            input_event.InputEventHandler(
                Script.reviewNextLine,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.
                #
                _("Moves flat review to the beginning of the next line."))

        self.inputEventHandlers["reviewEndHandler"] = \
            input_event.InputEventHandler(
                Script.reviewEnd,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  The end position is the last
                # bit of information in the window.
                #
                _("Moves flat review to the end position."))

        self.inputEventHandlers["reviewPreviousItemHandler"] = \
            input_event.InputEventHandler(
                Script.reviewPreviousItem,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Previous will go backwards
                # in the window until you reach the top (i.e., it will
                # wrap across lines if necessary).
                #
                _("Moves flat review to the previous item or word."))

        self.inputEventHandlers["reviewAboveHandler"] = \
            input_event.InputEventHandler(
                Script.reviewAbove,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Above in this case means
                # geographically above, as if you drew a vertical line
                # in the window.
                #
                _("Moves flat review to the word above the current word."))

        self.inputEventHandlers["reviewCurrentItemHandler"] = \
            input_event.InputEventHandler(
                Script.reviewCurrentItem,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  This command will speak the
                # current word or item.
                #
                _("Speaks the current flat review item or word."))

        self.inputEventHandlers["reviewSpellCurrentItemHandler"] = \
            input_event.InputEventHandler(
                Script.reviewSpellCurrentItem,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  This command will spell out
                # the current word or item letter by letter.
                #
                _("Spells the current flat review item or word."))

        self.inputEventHandlers["reviewPhoneticCurrentItemHandler"] = \
            input_event.InputEventHandler(
                Script.reviewPhoneticCurrentItem,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  This command will spell out
                # the current word or item phonetically, saying "Alpha"
                # for "a", "Bravo" for "b" and so on.
                #
                _("Phonetically spells the current flat review item or word."))

        self.inputEventHandlers["reviewCurrentAccessibleHandler"] = \
            input_event.InputEventHandler(
                Script.reviewCurrentAccessible,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  The flat review object is
                # typically something like a pushbutton, a label, or
                # some other GUI widget.  The 'speaks' means it will
                # speak the text associated with the object.
                #
                _("Speaks the current flat review object."))

        self.inputEventHandlers["reviewNextItemHandler"] = \
            input_event.InputEventHandler(
                Script.reviewNextItem,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Next will go forwards
                # in the window until you reach the end (i.e., it will
                # wrap across lines if necessary).
                #
                _("Moves flat review to the next item or word."))

        self.inputEventHandlers["reviewBelowHandler"] = \
            input_event.InputEventHandler(
                Script.reviewBelow,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Below in this case means
                # geographically below, as if you drew a vertical line
                # downward on the screen.
                #
                _("Moves flat review to the word below the current word."))

        self.inputEventHandlers["reviewPreviousCharacterHandler"] = \
            input_event.InputEventHandler(
                Script.reviewPreviousCharacter,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Previous will go backwards
                # in the window until you reach the top (i.e., it will
                # wrap across lines if necessary).
                #
                _("Moves flat review to the previous character."))

        self.inputEventHandlers["reviewEndOfLineHandler"] = \
            input_event.InputEventHandler(
                Script.reviewEndOfLine,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.
                #
                _("Moves flat review to the end of the line."))

        self.inputEventHandlers["reviewCurrentCharacterHandler"] = \
            input_event.InputEventHandler(
                Script.reviewCurrentCharacter,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Previous will go backwards
                # in the window until you reach the top (i.e., it will
                # wrap across lines if necessary).  The 'speaks' in
                # this case will be the spoken language form of the
                # character currently being reviewed.
                #
                _("Speaks the current flat review character."))

        self.inputEventHandlers["reviewSpellCurrentCharacterHandler"] = \
            input_event.InputEventHandler(
                Script.reviewSpellCurrentCharacter,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Previous will go backwards
                # in the window until you reach the top (i.e., it will
                # wrap across lines if necessary).  This command will
                # cause Orca to speak a phonetic representation of the
                # character currently being reviewed, saying "Alpha"
                # for "a", "Bravo" for "b" and so on.
                #
                _("Phonetically speaks the current flat review character."))

        self.inputEventHandlers["reviewNextCharacterHandler"] = \
            input_event.InputEventHandler(
                Script.reviewNextCharacter,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Next will go forwards
                # in the window until you reach the end (i.e., it will
                # wrap across lines if necessary).
                #
                _("Moves flat review to the next character."))

        self.inputEventHandlers["toggleTableCellReadModeHandler"] = \
            input_event.InputEventHandler(
                Script.toggleTableCellReadMode,
                # Translators: when users are navigating a table, they
                # sometimes want the entire row of a table read, or
                # they just want the current cell to be presented to them.
                #
                _("Toggles whether to read just the current table cell " \
                  "or the whole row."))

        self.inputEventHandlers["readCharAttributesHandler"] = \
            input_event.InputEventHandler(
                Script.readCharAttributes,
                # Translators: the attributes being presented are the
                # text attributes, such as bold, italic, font name,
                # font size, etc.
                #
                _("Reads the attributes associated with the current text " \
                  "character."))

        self.inputEventHandlers["reportScriptInfoHandler"] = \
            input_event.InputEventHandler(
                Script.reportScriptInfo,
                # Translators: this is a debug message that Orca users
                # will not normally see. It describes a debug routine
                # that outputs useful information on the current script
                #  via speech and braille. This information will be
                # helpful to script writers.
                #
                _("Reports information on current script."))

        self.inputEventHandlers["panBrailleLeftHandler"] = \
            input_event.InputEventHandler(
                Script.panBrailleLeft,
                # Translators: a refreshable braille display is an
                # external hardware device that presents braille
                # character to the user.  There are a limited number
                # of cells on the display (typically 40 cells).  Orca
                # provides the feature to build up a longer logical
                # line and allow the user to press buttons on the
                # braille display so they can pan left and right over
                # this line.
                #
                _("Pans the braille display to the left."),
                False) # Do not enable learn mode for this action

        self.inputEventHandlers["panBrailleRightHandler"] = \
            input_event.InputEventHandler(
                Script.panBrailleRight,
                # Translators: a refreshable braille display is an
                # external hardware device that presents braille
                # character to the user.  There are a limited number
                # of cells on the display (typically 40 cells).  Orca
                # provides the feature to build up a longer logical
                # line and allow the user to press buttons on the
                # braille display so they can pan left and right over
                # this line.
                #
                _("Pans the braille display to the right."),
                False) # Do not enable learn mode for this action

        self.inputEventHandlers["reviewBottomLeftHandler"] = \
            input_event.InputEventHandler(
                Script.reviewBottomLeft,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  The bottom left is the bottom
                # left of the window currently being reviewed.
                #
                _("Moves flat review to the bottom left."))

        self.inputEventHandlers["goBrailleHomeHandler"] = \
            input_event.InputEventHandler(
                Script.goBrailleHome,
                # Translators: the 'flat review' feature of Orca
                # allows the blind user to explore the text in a
                # window in a 2D fashion.  That is, Orca treats all
                # the text from all objects in a window (e.g.,
                # buttons, labels, etc.) as a sequence of words in a
                # sequence of lines.  The flat review feature allows
                # the user to explore this text by the {previous,next}
                # {line,word,character}.  Flat review is modal, and
                # the user can be exploring the window without changing
                # which object in the window which has focus.  The
                # feature used here will return the flat review to the
                # object with focus.
                #
                _("Returns to object with keyboard focus."))

        self.inputEventHandlers["enterLearnModeHandler"] = \
            input_event.InputEventHandler(
                Script.enterLearnMode,
                # Translators: Orca has a "Learn Mode" that will allow
                # the user to type any key on the keyboard and hear what
                # the effects of that key would be.  The effects might
                # be what Orca would do if it had a handler for the
                # particular key combination, or they might just be to
                # echo the name of the key if Orca doesn't have a handler.
                #
                _("Enters learn mode.  Press escape to exit learn mode."))

        self.inputEventHandlers["decreaseSpeechRateHandler"] = \
            input_event.InputEventHandler(
                speech.decreaseSpeechRate,
                # Translators: the speech rate is how fast the speech
                # synthesis engine will generate speech.
                #
                _("Decreases the speech rate."))

        self.inputEventHandlers["increaseSpeechRateHandler"] = \
            input_event.InputEventHandler(
                speech.increaseSpeechRate,
                # Translators: the speech rate is how fast the speech
                # synthesis engine will generate speech.
                #
                _("Increases the speech rate."))

        self.inputEventHandlers["decreaseSpeechPitchHandler"] = \
            input_event.InputEventHandler(
                speech.decreaseSpeechPitch,
                # Translators: the speech pitch is how high or low in
                # pitch/frequency the speech synthesis engine will
                # generate speech.
                #
                _("Decreases the speech pitch."))

        self.inputEventHandlers["increaseSpeechPitchHandler"] = \
            input_event.InputEventHandler(
                speech.increaseSpeechPitch,
                # Translators: the speech pitch is how high or low in
                # pitch/frequency the speech synthesis engine will
                # generate speech.
                #
                _("Increases the speech pitch."))

        self.inputEventHandlers["shutdownHandler"] = \
            input_event.InputEventHandler(
                orca.quitOrca,
                _("Quits Orca"))

        self.inputEventHandlers["preferencesSettingsHandler"] = \
            input_event.InputEventHandler(
                orca.showPreferencesGUI,
                # Translators: the preferences configuration dialog is
                # the dialog that allows users to set their preferences
                # for Orca.
                #
                _("Displays the preferences configuration dialog."))

        self.inputEventHandlers["appPreferencesSettingsHandler"] = \
            input_event.InputEventHandler(
                orca.showAppPreferencesGUI,
                # Translators: the application preferences configuration
                # dialog is the dialog that allows users to set their
                # preferences for a specific application within Orca.
                #
                _("Displays the application preferences configuration dialog."))

        self.inputEventHandlers["toggleSilenceSpeechHandler"] = \
            input_event.InputEventHandler(
                orca.toggleSilenceSpeech,
                # Translators: Orca allows the user to turn speech synthesis
                # on or off.  We call it 'silencing'.
                #
                _("Toggles the silencing of speech."))

        self.inputEventHandlers["listAppsHandler"] = \
            input_event.InputEventHandler(
                Script.printAppsHandler,
                # Translators: this is a debug message that Orca users
                # will not normally see. It describes a debug routine
                # that prints a list of all known applications currently
                # running on the desktop, to stdout.
                #
                _("Prints a debug listing of all known applications to the " \
                "console where Orca is running."))

        self.inputEventHandlers["cycleDebugLevelHandler"] = \
            input_event.InputEventHandler(
                orca.cycleDebugLevel,
                # Translators: this is a debug message that Orca users
                # will not normally see. It describes a debug routine
                # that allows the user to adjust the level of debug
                # information that Orca generates at run time.
                #
                _("Cycles the debug level at run time."))

        self.inputEventHandlers["printActiveAppHandler"] = \
            input_event.InputEventHandler(
                Script.printActiveAppHandler,
                # Translators: this is a debug message that Orca users
                # will not normally see. It describes a debug routine
                # that prints useful debugging information to the console,
                # for the application that is currently running (has focus).
                #
                _("Prints debug information about the currently active " \
                "application to the console where Orca is running."))

        self.inputEventHandlers["printAncestryHandler"] = \
            input_event.InputEventHandler(
                Script.printAncestryHandler,
                # Translators: this is a debug message that Orca users
                # will not normally see. It describes a debug routine
                # that will take the component in the currently running
                # application that has focus, and print debug information
                # to the console giving its component ancestry (i.e. all
                # the components that are its descendants in the component
                # tree).
                #
                _("Prints debug information about the ancestry of the object " \
                "with focus."))

        self.inputEventHandlers["printHierarchyHandler"] = \
            input_event.InputEventHandler(
                Script.printHierarchyHandler,
                # Translators: this is a debug message that Orca users
                # will not normally see. It describes a debug routine
                # that will take the currently running application, and
                # print debug information to the console giving its
                # component hierarchy (i.e. all the components and all
                # their descendants in the component tree).
                #
                _("Prints debug information about the application with focus."))

        self.inputEventHandlers["printMemoryUsageHandler"] = \
            input_event.InputEventHandler(
                Script.printMemoryUsageHandler,
                # Translators: this is a debug message that Orca users
                # will not normally see. It describes a debug routine
                # that will print Orca memory usage information.
                #
                _("Prints memory usage information."))

        self.inputEventHandlers["bookmarkCurrentWhereAmI"] = \
            input_event.InputEventHandler(
                Script.bookmarkCurrentWhereAmI,
                # Translators: this command announces information regarding
                # the relationship of the given bookmark to the current
                # position
                #
                _("Bookmark where am I with respect to current position."))

        self.inputEventHandlers["goToBookmark"] = \
            input_event.InputEventHandler(
                Script.goToBookmark,
                # Translators: this command moves the current position to the
                # location stored at the bookmark.
                #
                _("Go to bookmark."))

        self.inputEventHandlers["addBookmark"] = \
            input_event.InputEventHandler(
                Script.addBookmark,
                # Translators: this event handler binds an in-page accessible
                # object location to the given input key command.
                #
                _("Add bookmark."))

        self.inputEventHandlers["saveBookmarks"] = \
            input_event.InputEventHandler(
                Script.saveBookmarks,
                # Translators: this event handler saves all bookmarks for the
                # current application to disk.
                #
                _("Save bookmarks."))

        self.inputEventHandlers["goToNextBookmark"] = \
            input_event.InputEventHandler(
                Script.goToNextBookmark,
                # Translators: this event handler cycles through the registered
                # bookmarks and takes the user to the next bookmark location.
                #
                _("Go to next bookmark location."))

        self.inputEventHandlers["goToPrevBookmark"] = \
            input_event.InputEventHandler(
                Script.goToPrevBookmark,
                # Translators: this event handler cycles through the
                # registered bookmarks and takes the user to the previous
                # bookmark location.
                #
                _("Go to previous bookmark location."))

        self.inputEventHandlers["toggleColorEnhancementsHandler"] = \
            input_event.InputEventHandler(
                mag.toggleColorEnhancements,
                # Translators: "color enhancements" are changes users can
                # make to the appearance of the screen to make things easier
                # to see, such as inverting the colors or applying a tint.
                # This command toggles these enhancements on/off.
                #
                _("Toggles color enhancements."))

        self.inputEventHandlers["toggleMouseEnhancementsHandler"] = \
            input_event.InputEventHandler(
                mag.toggleMouseEnhancements,
                # Translators: "mouse enhancements" are changes users can
                # make to the appearance of the mouse pointer to make it
                # easier to see, such as increasing its size, changing its
                # color, and surrounding it with crosshairs.  This command
                # toggles these enhancements on/off.
                #
                _("Toggles mouse enhancements."))

        self.inputEventHandlers["increaseMagnificationHandler"] = \
            input_event.InputEventHandler(
                mag.increaseMagnification,
                # Translators: this command increases the magnification
                # level.
                #
                _("Increases the magnification level."))

        self.inputEventHandlers["decreaseMagnificationHandler"] = \
            input_event.InputEventHandler(
                mag.decreaseMagnification,
                # Translators: this command decreases the magnification
                # level.
                #
                _("Decreases the magnification level."))

        self.inputEventHandlers["toggleMagnifierHandler"] = \
            input_event.InputEventHandler(
                mag.toggleMagnifier,
                # Translators: Orca allows the user to turn the magnifier
                # on or off. This command not only toggles magnification,
                # but also all of the color and pointer customizations
                # made through the magnifier.
                #
                _("Toggles the magnifier."))

        self.inputEventHandlers["cycleZoomerTypeHandler"] = \
            input_event.InputEventHandler(
                mag.cycleZoomerType,
                # Translators: the user can choose between several different
                # types of magnification, including full screen and split
                # screen.  The "position" here refers to location of the
                # magnifier.
                #
                _("Cycles to the next magnifier position."))

        self.inputEventHandlers["toggleMouseReviewHandler"] = \
            input_event.InputEventHandler(
                mouse_review.toggle,
                # Translators: Orca allows the item under the pointer to
                # be spoken. This toggles the feature.
                #
                _("Toggle mouse review mode."))

        self.inputEventHandlers["bypassNextCommandHandler"] = \
            input_event.InputEventHandler(
                Script.bypassNextCommand,
                # Translators: Orca normally intercepts all keyboard
                # commands and only passes them along to the current
                # application when they are not Orca commands.  This
                # command causes the next command issued to be passed
                # along to the current application, bypassing Orca's
                # interception of it.
                #
                _("Passes the next command on to the current application."))

    def getInputEventHandlerKey(self, inputEventHandler):
        """Returns the name of the key that contains an inputEventHadler
        passed as argument
        """

        for keyName, handler in self.inputEventHandlers.iteritems():
            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["mouse:button"]                           = \
            self.onMouseButton
        listeners["object:property-change:accessible-name"] = \
            self.onNameChanged
        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:link-selected"]                   = \
            self.onLinkSelected
        listeners["object:state-changed:active"]            = \
            self.onStateChanged
        listeners["object:state-changed:focused"]           = \
            self.onStateChanged
        listeners["object:state-changed:showing"]           = \
            self.onStateChanged
        listeners["object:state-changed:checked"]           = \
            self.onStateChanged
        listeners["object:state-changed:pressed"]           = \
            self.onStateChanged
        listeners["object:state-changed:indeterminate"]     = \
            self.onStateChanged
        listeners["object:state-changed:expanded"]          = \
            self.onStateChanged
        listeners["object:state-changed:selected"]          = \
            self.onStateChanged
        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["window:activate"]                        = \
            self.onWindowActivated
        listeners["window:deactivate"]                      = \
            self.onWindowDeactivated
        listeners["window:create"]                          = \
            self.noOp

        return listeners

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

        keyBindings = keybindings.KeyBindings()

        # We want the user to be able to combine modifiers with the
        # mouse click (e.g. to Shift+Click and select), therefore we
        # do not "care" about the modifiers.
        #
        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Divide",
                settings.NO_MODIFIER_MASK,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["leftClickReviewItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Multiply",
                settings.NO_MODIFIER_MASK,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["rightClickReviewItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Subtract",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["toggleFlatReviewModeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Add",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["sayAllHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Enter",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["whereAmIBasicHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Enter",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["whereAmIDetailedHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Enter",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["getTitleHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Enter",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["getStatusBarHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Delete",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["findHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Delete",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["findNextHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Delete",
                settings.defaultModifierMask,
                settings.ORCA_SHIFT_MODIFIER_MASK,
                self.inputEventHandlers["findPreviousHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_7",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Home",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_7",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewHomeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Home",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewHomeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_8",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentLineHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_8",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentLineHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_8",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPhoneticCurrentLineHandler"],
                3))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Up",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentLineHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Up",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentLineHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Up",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPhoneticCurrentLineHandler"],
                3))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_9",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Page_Up",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_9",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewEndHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Page_Up",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewEndHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_4",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Left",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_4",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewAboveHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Left",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewAboveHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_5",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentItemHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_5",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentItemHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_5",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPhoneticCurrentItemHandler"],
                3))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Begin",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentItemHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Begin",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentItemHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Begin",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPhoneticCurrentItemHandler"],
                3))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_5",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentAccessibleHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Begin",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentAccessibleHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_6",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Right",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_6",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewBelowHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Right",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewBelowHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_1",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousCharacterHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_End",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousCharacterHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_1",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewEndOfLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_End",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewEndOfLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_2",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentCharacterHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_2",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentCharacterHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Down",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentCharacterHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Down",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentCharacterHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_3",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextCharacterHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "KP_Page_Down",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextCharacterHandler"]))

        return keyBindings

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

        keyBindings = keybindings.KeyBindings()

        # We want the user to be able to combine modifiers with the
        # mouse click (e.g. to Shift+Click and select), therefore we
        # do not "care" about the modifiers (other than the Orca
        # modifier).
        #
        keyBindings.add(
            keybindings.KeyBinding(
                "7",
                settings.ORCA_MODIFIER_MASK,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["leftClickReviewItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "8",
                settings.ORCA_MODIFIER_MASK,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["rightClickReviewItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "p",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["toggleFlatReviewModeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "semicolon",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["sayAllHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "Return",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["whereAmIBasicHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "Return",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["whereAmIDetailedHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "slash",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["getTitleHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "slash",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["getStatusBarHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "bracketleft",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["findHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "bracketright",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["findNextHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "bracketright",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_MODIFIER_MASK,
                self.inputEventHandlers["findPreviousHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "u",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "u",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_MODIFIER_MASK,
                self.inputEventHandlers["reviewHomeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "i",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentLineHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "i",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentLineHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "i",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewPhoneticCurrentLineHandler"],
                3))

        keyBindings.add(
            keybindings.KeyBinding(
                "o",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "o",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_MODIFIER_MASK,
                self.inputEventHandlers["reviewEndHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "j",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "j",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_MODIFIER_MASK,
                self.inputEventHandlers["reviewAboveHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "k",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentItemHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "k",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentItemHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "k",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewPhoneticCurrentItemHandler"],
                3))

        keyBindings.add(
            keybindings.KeyBinding(
                "k",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentAccessibleHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "l",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextItemHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "l",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_MODIFIER_MASK,
                self.inputEventHandlers["reviewBelowHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "m",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewPreviousCharacterHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "m",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_MODIFIER_MASK,
                self.inputEventHandlers["reviewEndOfLineHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "comma",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewCurrentCharacterHandler"],
                1))

        keyBindings.add(
            keybindings.KeyBinding(
                "comma",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewSpellCurrentCharacterHandler"],
                2))

        keyBindings.add(
            keybindings.KeyBinding(
                "period",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["reviewNextCharacterHandler"]))

        return keyBindings

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

        Returns an instance of keybindings.KeyBindings.
        """

        keyBindings = script.Script.getKeyBindings(self)

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

        keyBindings.add(
            keybindings.KeyBinding(
                "Num_Lock",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["showZonesHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "F11",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["toggleTableCellReadModeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "SunF36",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["toggleTableCellReadModeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "f",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["readCharAttributesHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "h",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["enterLearnModeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "q",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["shutdownHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "space",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["preferencesSettingsHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "space",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_MODIFIER_MASK,
                self.inputEventHandlers["appPreferencesSettingsHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "s",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["toggleSilenceSpeechHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "End",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_ALT_MODIFIER_MASK,
                self.inputEventHandlers["listAppsHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "Home",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_ALT_MODIFIER_MASK,
                self.inputEventHandlers["reportScriptInfoHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "Page_Up",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_ALT_MODIFIER_MASK,
                self.inputEventHandlers["printAncestryHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "Page_Down",
                settings.defaultModifierMask,
                settings.ORCA_CTRL_ALT_MODIFIER_MASK,
                self.inputEventHandlers["printHierarchyHandler"]))

        #####################################################################
        #                                                                   #
        #  Bookmark key bindings                                            #
        #                                                                   #
        #####################################################################
        # key binding to save bookmark information to disk
        keyBindings.add(
            keybindings.KeyBinding(
                "b",
                settings.defaultModifierMask,
                settings.ORCA_ALT_MODIFIER_MASK,
                self.inputEventHandlers["saveBookmarks"]))
        # key binding to move to the previous bookmark
        keyBindings.add(
            keybindings.KeyBinding(
                "b",
                settings.defaultModifierMask,
                settings.ORCA_SHIFT_MODIFIER_MASK,
                self.inputEventHandlers["goToPrevBookmark"]))
        # key binding to move to the next bookmark
        keyBindings.add(
            keybindings.KeyBinding(
                "b",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["goToNextBookmark"]))

        # key bindings for '1' through '6' for relevant commands
        for key in xrange(1, 7):
            # 'Add bookmark' key bindings
            keyBindings.add(
                keybindings.KeyBinding(
                    str(key),
                    settings.defaultModifierMask,
                    settings.ORCA_ALT_MODIFIER_MASK,
                    self.inputEventHandlers["addBookmark"]))

            # 'Go to bookmark' key bindings
            keyBindings.add(
                keybindings.KeyBinding(
                    str(key),
                    settings.defaultModifierMask,
                    settings.ORCA_MODIFIER_MASK,
                    self.inputEventHandlers["goToBookmark"]))

            # key binding for WhereAmI information with respect to root acc
            keyBindings.add(
                keybindings.KeyBinding(
                    str(key),
                    settings.defaultModifierMask,
                    settings.SHIFT_ALT_MODIFIER_MASK,
                    self.inputEventHandlers["bookmarkCurrentWhereAmI"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "BackSpace",
                settings.defaultModifierMask,
                settings.ORCA_MODIFIER_MASK,
                self.inputEventHandlers["bypassNextCommandHandler"]))

        #####################################################################
        #                                                                   #
        #  Unbound handlers                                                 #
        #                                                                   #
        #####################################################################

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["reportScriptInfoHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["cycleDebugLevelHandler"]))

        if settings.debugMemoryUsage:
            keyBindings.add(
                keybindings.KeyBinding(
                    "",
                    settings.defaultModifierMask,
                    settings.NO_MODIFIER_MASK,
                    self.inputEventHandlers["printMemoryUsageHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["decreaseSpeechRateHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["increaseSpeechRateHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["decreaseSpeechPitchHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["increaseSpeechPitchHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["toggleColorEnhancementsHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["toggleMouseEnhancementsHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["increaseMagnificationHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["decreaseMagnificationHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["toggleMagnifierHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["cycleZoomerTypeHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["panBrailleLeftHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["panBrailleRightHandler"]))

        keyBindings.add(
            keybindings.KeyBinding(
                "",
                settings.defaultModifierMask,
                settings.NO_MODIFIER_MASK,
                self.inputEventHandlers["toggleMouseReviewHandler"]))

        keyBindings = settings.overrideKeyBindings(self, keyBindings)

        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.
        """
        brailleBindings = script.Script.getBrailleBindings(self)
        brailleBindings[braille.CMD_FWINLT]   = \
            self.inputEventHandlers["panBrailleLeftHandler"]
        brailleBindings[braille.CMD_FWINRT]   = \
            self.inputEventHandlers["panBrailleRightHandler"]
        brailleBindings[braille.CMD_LNUP]     = \
            self.inputEventHandlers["reviewAboveHandler"]
        brailleBindings[braille.CMD_LNDN]     = \
            self.inputEventHandlers["reviewBelowHandler"]
        brailleBindings[braille.CMD_FREEZE]   = \
            self.inputEventHandlers["toggleFlatReviewModeHandler"]
        brailleBindings[braille.CMD_TOP_LEFT] = \
            self.inputEventHandlers["reviewHomeHandler"]
        brailleBindings[braille.CMD_BOT_LEFT] = \
            self.inputEventHandlers["reviewBottomLeftHandler"]
        brailleBindings[braille.CMD_HOME]     = \
            self.inputEventHandlers["goBrailleHomeHandler"]

        return brailleBindings

    def processKeyboardEvent(self, keyboardEvent):
        """Processes the given keyboard event. It uses the super
        class equivalent to do most of the work. The only thing done here
        is to detect when the user is trying to get out of learn mode.

        Arguments:
        - keyboardEvent: an instance of input_event.KeyboardEvent
        """

        return script.Script.processKeyboardEvent(self, keyboardEvent)

    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.]]]
        #
        text = context.obj.queryText()
        if progressType == speechserver.SayAllContext.PROGRESS:
            #print "PROGRESS", context.utterance, context.currentOffset
            #obj = context.obj
            #[x, y, width, height] = obj.text.getCharacterExtents(
            #    context.currentOffset, 0)
            #print context.currentOffset, x, y, width, height
            #self.drawOutline(x, y, width, height)
            return
        elif progressType == speechserver.SayAllContext.INTERRUPTED:
            #print "INTERRUPTED", context.utterance, context.currentOffset
            text.setCaretOffset(context.currentOffset)
        elif progressType == speechserver.SayAllContext.COMPLETED:
            #print "COMPLETED", context.utterance, context.currentOffset
            orca.setLocusOfFocus(None, context.obj, False)
            text.setCaretOffset(context.currentOffset)

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

    def sayAll(self, inputEvent):
        clickCount = self.getClickCount()
        doubleClick = clickCount == 2
        self.lastSayAllEvent = inputEvent

        if doubleClick:
            # Try to "say all" for the current dialog/window by flat
            # reviewing everything. See bug #354462 for more details.
            #
            context = self.getFlatReviewContext()

            utterances = []
            context.goBegin()
            while True:
                [wordString, x, y, width, height] = \
                         context.getCurrent(flat_review.Context.ZONE)

                utterances.append(wordString)

                moved = context.goNext(flat_review.Context.ZONE,
                                       flat_review.Context.WRAP_LINE)

                if not moved:
                    break

            speech.speakUtterances(utterances)

        elif self.isTextArea(orca_state.locusOfFocus):
            try:
                orca_state.locusOfFocus.queryText()
            except NotImplementedError:
                utterances = self.speechGenerator.getSpeech(
                             orca_state.locusOfFocus, False)
                utterances.extend(self.tutorialGenerator.getTutorial(
                           orca_state.locusOfFocus, False))
                speech.speakUtterances(utterances)
            except AttributeError:
                pass
            else:
                speech.sayAll(self.textLines(orca_state.locusOfFocus),
                              self.__sayAllProgressCallback)

        return True

    def isTextArea(self, obj):
        """Returns True if obj is a GUI component that is for entering text.

        Arguments:
        - obj: an accessible
        """
        return obj and \
            obj.getRole() in (pyatspi.ROLE_TEXT,
                              pyatspi.ROLE_PARAGRAPH,
                              pyatspi.ROLE_TERMINAL)

    def isReadOnlyTextArea(self, obj):
        """Returns True if obj is a text entry area that is read only."""
        state = obj.getState()
        readOnly = self.isTextArea(obj) \
                   and state.contains(pyatspi.STATE_FOCUSABLE) \
                   and not state.contains(pyatspi.STATE_EDITABLE)
        debug.println(debug.LEVEL_ALL,
                      "default.py:isReadOnlyTextArea=%s for %s" \
                      % (readOnly, debug.getAccessibleDetails(obj)))
        return readOnly

    def getText(self, obj, startOffset, endOffset):
        """Returns the substring of the given object's text specialization.

        Arguments:
        - obj: an accessible supporting the accessible text specialization
        - startOffset: the starting character position
        - endOffset: the ending character position
        """
        return obj.queryText().getText(startOffset, endOffset)

    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.getText(obj, startOffset, endOffset)

        if len(phrase) and phrase != "\n":
            if phrase.isupper():
                voice = self.voices[settings.UPPERCASE_VOICE]
            else:
                voice = self.voices[settings.DEFAULT_VOICE]

            phrase = self.adjustForRepeats(phrase)
            speech.speak(phrase, voice)
        else:
            # Speak blank line if appropriate.
            #
            self.sayCharacter(obj)

    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
        """

        # Get the AccessibleText interface of the provided object
        #
        [line, caretOffset, startOffset] = self.getTextLineAtCaret(obj)
        debug.println(debug.LEVEL_FINEST, \
            "sayLine: line=<%s>, len=%d, start=%d, " % \
            (line, len(line), startOffset))
        debug.println(debug.LEVEL_FINEST, \
            "caret=%d, speakBlankLines=%s" % \
            (caretOffset, settings.speakBlankLines))

        if len(line) and line != "\n":
            if line.isupper():
                voice = self.voices[settings.UPPERCASE_VOICE]
            else:
                voice = self.voices[settings.DEFAULT_VOICE]

            if settings.enableSpeechIndentation:
                self.speakTextIndentation(obj, line)
            line = self.adjustForLinks(obj, line, startOffset)
            line = self.adjustForRepeats(line)
            speech.speak(line, voice)
        else:
            # Speak blank line if appropriate.
            #
            self.sayCharacter(obj)

    def sayWord(self, obj):
        """Speaks the word at the caret.  [[[TODO: WDW - what if there is no
        word at the caret?]]]

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

        text = obj.queryText()
        offset = text.caretOffset
        lastKey = orca_state.lastNonModifierKeyEvent.event_string
        lastWord = orca_state.lastWord

        [word, startOffset, endOffset] = \
            text.getTextAtOffset(offset,
                                 pyatspi.TEXT_BOUNDARY_WORD_START)

        # Speak a newline if a control-right-arrow or control-left-arrow
        # was used to cross a line boundary. Handling is different for
        # the two keys since control-right-arrow places the cursor after
        # the last character in a word, but control-left-arrow places
        # the cursor at the beginning of a word.
        #
        if lastKey == "Right" and len(lastWord) > 0:
            lastChar = lastWord[len(lastWord) - 1]
            if lastChar == "\n" and lastWord != word:
                voice = self.voices[settings.DEFAULT_VOICE]
                speech.speakCharacter("\n", voice)

        if lastKey == "Left" and len(word) > 0:
            lastChar = word[len(word) - 1]
            if lastChar == "\n" and lastWord != word:
                voice = self.voices[settings.DEFAULT_VOICE]
                speech.speakCharacter("\n", voice)

        if self.getLinkIndex(obj, offset) >= 0:
            voice = self.voices[settings.HYPERLINK_VOICE]
        elif word.isupper():
            voice = self.voices[settings.UPPERCASE_VOICE]
        else:
            voice = self.voices[settings.DEFAULT_VOICE]

        word = self.adjustForRepeats(word)
        orca_state.lastWord = word
        speech.speak(word, voice)

    def speakTextIndentation(self, obj, line):
        """Speaks a summary of the number of spaces and/or tabs at the
        beginning of the given line.

        Arguments:
        - obj: the text object.
        - line: the string to check for spaces and tabs.
        """

        # For the purpose of speaking the text indentation, replace
        # occurances of UTF-8 '\302\240' (non breaking space) with
        # spaces.
        #
        line = line.replace("\302\240",  " ")
        line = line.decode("UTF-8")

        spaceCount = 0
        tabCount = 0
        utterance = ""
        offset = 0
        while True:
            while (offset < len(line)) and line[offset] == ' ':
                spaceCount += 1
                offset += 1
            if spaceCount:
                # Translators: this is the number of space characters on a line
                # of text.
                #
                utterance += ngettext("%d space",
                                      "%d spaces",
                                      spaceCount) % spaceCount + " "

            while (offset < len(line)) and line[offset] == '\t':
                tabCount += 1
                offset += 1
            if tabCount:
                # Translators: this is the number of tab characters on a line
                # of text.
                #
                utterance += ngettext("%d tab",
                                      "%d tabs",
                                      tabCount) % tabCount + " "

            if not (spaceCount  or tabCount):
                break
            spaceCount  = tabCount = 0

        if len(utterance):
            speech.speak(utterance)

    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
 
        offset = text.caretOffset - 1
        previousOffset = text.caretOffset - 2
        if (offset < 0 or previousOffset < 0):
            return
 
        [currentChar, startOffset, endOffset] = \
            text.getTextAtOffset(offset, pyatspi.TEXT_BOUNDARY_CHAR)
        [previousChar, startOffset, endOffset] = \
            text.getTextAtOffset(previousOffset, pyatspi.TEXT_BOUNDARY_CHAR)
        if not self.isSentenceDelimiter(currentChar, previousChar):
            return
 
        # 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,
                                     pyatspi.TEXT_BOUNDARY_CHAR)
            [previousChar, startOffset, endOffset] = \
                text.getTextAtOffset(sentenceStartOffset-1,
                                     pyatspi.TEXT_BOUNDARY_CHAR)
            if self.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
        else:
            sentence = self.getText(obj, sentenceStartOffset + 1,
                                         sentenceEndOffset + 1)
 
        if self.getLinkIndex(obj, sentenceStartOffset + 1) >= 0:
            voice = self.voices[settings.HYPERLINK_VOICE]
        elif sentence.isupper():
            voice = self.voices[settings.UPPERCASE_VOICE]
        else:
            voice = self.voices[settings.DEFAULT_VOICE]
 
        sentence = self.adjustForRepeats(sentence)
        speech.speak(sentence, voice)
 
    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

        if not offset:
            offset = text.caretOffset - 1
        if (offset < 0):
            return

        [char, startOffset, endOffset] = \
            text.getTextAtOffset( \
                offset,
                pyatspi.TEXT_BOUNDARY_CHAR)
        if not self.isWordDelimiter(char):
            return

        # 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,
                    pyatspi.TEXT_BOUNDARY_CHAR)
            if self.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
        else:
            word = self.getText(obj, wordStartOffset + 1, wordEndOffset + 1)

        if self.getLinkIndex(obj, wordStartOffset + 1) >= 0:
            voice = self.voices[settings.HYPERLINK_VOICE]
        elif word.isupper():
            voice = self.voices[settings.UPPERCASE_VOICE]
        else:
            voice = self.voices[settings.DEFAULT_VOICE]

        word = self.adjustForRepeats(word)
        speech.speak(word, voice)

    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).
        #
        try:
            mods = orca_state.lastInputEvent.modifiers
            eventString = orca_state.lastInputEvent.event_string
        except:
            mods = 0
            eventString = ""

        if (mods & settings.SHIFT_MODIFIER_MASK) \
           and eventString in ["Right", "Down"]:
            offset -= 1

        character, startOffset, endOffset = \
            text.getTextAtOffset(offset, pyatspi.TEXT_BOUNDARY_CHAR)
        if not character:
            character = "\n"

        if self.getLinkIndex(obj, offset) >= 0:
            voice = self.voices[settings.HYPERLINK_VOICE]
        elif character.decode("UTF-8").isupper():
            voice = self.voices[settings.UPPERCASE_VOICE]
        else:
            voice = self.voices[settings.DEFAULT_VOICE]

        debug.println(debug.LEVEL_FINEST, \
            "sayCharacter: char=<%s>, startOffset=%d, " % \
            (character, startOffset))
        debug.println(debug.LEVEL_FINEST, \
            "caretOffset=%d, endOffset=%d, speakBlankLines=%s" % \
            (offset, endOffset, settings.speakBlankLines))

        if character == "\n":
            line = text.getTextAtOffset(max(0, offset),
                                        pyatspi.TEXT_BOUNDARY_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 settings.speakBlankLines:
                    # Translators: "blank" is a short word to mean the
                    # user has navigated to an empty line.
                    #
                    speech.speak(_("blank"), voice, False)
                return

        speech.speakCharacter(character, voice)

    def isFunctionalDialog(self, obj):
        """Returns true if the window is a functioning as a dialog.
        This method should be subclassed by application scripts as needed.
        """

        return False

    def getUnfocusedAlertAndDialogCount(self, obj):
        """If the current application has one or more alert or dialog
        windows and the currently focused window is not an alert or a dialog,
        return a count of the number of alert and dialog windows, otherwise
        return a count of zero.

        Arguments:
        - obj: the Accessible object

        Returns the alert and dialog count.
        """

        alertAndDialogCount = 0
        app = obj.getApplication()
        window = self.getTopLevel(obj)
        if window and window.getRole() != pyatspi.ROLE_ALERT and \
           window.getRole() != pyatspi.ROLE_DIALOG and \
           not self.isFunctionalDialog(window):
            for child in app:
                if child.getRole() == pyatspi.ROLE_ALERT or \
                   child.getRole() == pyatspi.ROLE_DIALOG or \
                   self.isFunctionalDialog(child):
                    alertAndDialogCount += 1

        return alertAndDialogCount

    def presentTooltip(self, obj):
        """
        Speaks the tooltip for the current object of interest.
        """

        # The tooltip is generally the accessible description. If
        # the description is not set, present the text that is
        # spoken when the object receives keyboard focus.
        #
        text = ""
        if obj.description:
            text = obj.description
        else:
            # Reuse the "where am I" algorithm.
            text = self.whereAmI.getObjLabelAndName(obj)

        debug.println(debug.LEVEL_FINEST, "presentTooltip: text='%s'" % text)
        if text != "":
            braille.displayMessage(text)
            speech.speak(text)

    def doWhereAmI(self, inputEvent, basicOnly):
        """Peforms the whereAmI operation.

        Arguments:
        - inputEvent:     The original inputEvent
        """

        obj = orca_state.locusOfFocus
        self.updateBraille(obj)

        return self.whereAmI.whereAmI(obj, basicOnly)

    def whereAmIBasic(self, inputEvent):
        """Speaks basic information about the current object of interest.
        """

        self.doWhereAmI(inputEvent, True)

    def whereAmIDetailed(self, inputEvent):
        """Speaks detailed/custom information about the current object of
        interest.
        """

        self.doWhereAmI(inputEvent, False)

    def getTitle(self, inputEvent):
        """Speaks the title of the window with focus.
        """

        obj = orca_state.locusOfFocus
        self.updateBraille(obj)

        return self.whereAmI.speakTitle(orca_state.locusOfFocus)

    def getStatusBar(self, inputEvent):
        """Speaks the contents of the status bar of the window with focus.
        """

        obj = orca_state.locusOfFocus
        self.updateBraille(obj)

        return self.whereAmI.speakStatusBar(orca_state.locusOfFocus)

    def findCommonAncestor(self, a, b):
        """Finds the common ancestor between Accessible a and Accessible b.

        Arguments:
        - a: Accessible
        - b: Accessible
        """

        debug.println(debug.LEVEL_FINEST,
                      "default.findCommonAncestor...")

        if (not a) or (not b):
            return None

        if a == b:
            return a

        aParents = [a]
        try:
            parent = a.parent
            while parent and (parent.parent != parent):
                aParents.append(parent)
                parent = parent.parent
            aParents.reverse()
        except:
            debug.printException(debug.LEVEL_FINEST)

        bParents = [b]
        try:
            parent = b.parent
            while parent and (parent.parent != parent):
                bParents.append(parent)
                parent = parent.parent
            bParents.reverse()
        except:
            debug.printException(debug.LEVEL_FINEST)

        commonAncestor = None

        maxSearch = min(len(aParents), len(bParents))
        i = 0
        while i < maxSearch:
            if self.isSameObject(aParents[i], bParents[i]):
                commonAncestor = aParents[i]
                i += 1
            else:
                break

        debug.println(debug.LEVEL_FINEST,
                      "...default.findCommonAncestor")

        return commonAncestor

    def handleProgressBarUpdate(self, event, obj):
        """Determine whether this progress bar event should be spoken or not.
        It should be spoken if:
        1/ settings.enableProgressBarUpdates is True.
        2/ The application with the progress bar has focus.
        3/ The time of this event exceeds the
           settings.progressBarUpdateInterval value.  This value
           indicates the time (in seconds) between potential spoken
           progress bar updates.
        4/ The new value of the progress bar (converted to an integer),
           is different from the last one or equals 100 (i.e complete).

        Arguments:
        - event: if not None, the Event that caused this to happen
        - obj:  the Accessible progress bar object.
        """

        if settings.enableProgressBarUpdates:
            if orca_state.locusOfFocus and \
               orca_state.locusOfFocus.getApplication() == obj.getApplication():
                currentTime = time.time()

                # Check for defunct progress bars. Get rid of them if they
                # are all defunct. Also find out which progress bar was
                # the most recently updated.
                #
                defunctBars = 0
                mostRecentUpdate = [obj, 0]
                for key, value in self.lastProgressBarTime.items():
                    if value > mostRecentUpdate[1]:
                        mostRecentUpdate = [key, value]
                    try:
                        isDefunct = \
                            key.getState().contains(pyatspi.STATE_DEFUNCT)
                    except:
                        isDefunct = True
                    if isDefunct:
                        defunctBars += 1

                if defunctBars == len(self.lastProgressBarTime):
                    self.lastProgressBarTime = {}
                    self.lastProgressBarValue = {}

                # If this progress bar is not already known, create initial
                # values for it.
                #
                if obj not in self.lastProgressBarTime:
                    self.lastProgressBarTime[obj] = 0.0
                if obj not in self.lastProgressBarValue:
                    self.lastProgressBarValue[obj] = None

                lastProgressBarTime = self.lastProgressBarTime[obj]
                lastProgressBarValue = self.lastProgressBarValue[obj]
                value = obj.queryValue()
                percentValue = int((value.currentValue / \
                    (value.maximumValue - value.minimumValue)) * 100.0)

                if (currentTime - lastProgressBarTime) > \
                       settings.progressBarUpdateInterval \
                   or percentValue == 100:
                    if lastProgressBarValue != percentValue:
                        utterances = []

                        # There may be cases when more than one progress
                        # bar is updating at the same time in a window.
                        # If this is the case, then speak the index of this
                        # progress bar in the dictionary of known progress
                        # bars, as well as the value. But only speak the
                        # index if this progress bar was not the most
                        # recently updated to prevent chattiness.
                        #
                        if len(self.lastProgressBarTime) > 1:
                            index = 0
                            for key in self.lastProgressBarTime.keys():
                                if key == obj and key != mostRecentUpdate[0]:
                                    # Translators: this is an index value
                                    # so that we can tell which progress bar
                                    # we are referring to.
                                    #
                                    label = _("Progress bar %d.") % (index + 1)
                                    utterances.append(label)
                                else:
                                    index += 1

                        # Translators: this is the percentage value of a
                        # progress bar.
                        #
                        percentage = _("%d percent.") % percentValue + " "

                        utterances.append(percentage)
                        speech.speakUtterances(utterances)

                        self.lastProgressBarTime[obj] = currentTime
                        self.lastProgressBarValue[obj] = percentValue

    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
        """

        if newLocusOfFocus \
           and newLocusOfFocus.getState().contains(pyatspi.STATE_DEFUNCT):
            return

        try:
            if self.findCommandRun:
                # Then the Orca 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:
            pass

        if newLocusOfFocus:
            mag.magnifyAccessible(event, newLocusOfFocus)

        # We always automatically go back to focus tracking mode when
        # the focus changes.
        #
        if self.flatReviewContext:
            self.toggleFlatReviewMode()

        # [[[TODO: WDW - HACK because parents that manage their descendants
        # can give us a different object each time we ask for the same
        # exact child.  So...we do a check here to see if the old object
        # and new object have the same index in the parent and if they
        # have the same name.  If so, then they are likely to be the same
        # object.  The reason we check for the name here is a small sanity
        # check.  This whole algorithm could fail because one might be
        # deleting/adding identical elements from/to a list or table, thus
        # the objects really could be different even though they seem the
        # same.  Logged as bug 319675.]]]
        #
        if self.isSameObject(oldLocusOfFocus, newLocusOfFocus):
            return

        # Well...now that we got that behind us, let's do what we're supposed
        # to do.
        #
        if oldLocusOfFocus:
            oldParent = oldLocusOfFocus.parent
        else:
            oldParent = None

        if newLocusOfFocus:
            newParent = newLocusOfFocus.parent
        else:
            newParent = None

        # Clear the point of reference.
        # If the point of reference is a cell, we want to keep the
        # table-related points of reference.
        if oldParent is not None and oldParent == newParent and \
              newParent.getRole() in [pyatspi.ROLE_TABLE,
                                      pyatspi.ROLE_TREE_TABLE]:
            for key in self.pointOfReference.keys():
                if key not in ('lastRow', 'lastColumn'):
                    del self.pointOfReference[key]
        else:
            self.pointOfReference = {}

        if newLocusOfFocus:
            self.updateBraille(newLocusOfFocus)

            utterances = []

            # Now figure out how of the container context changed and
            # speech just what is different.
            #
            commonAncestor = self.findCommonAncestor(oldLocusOfFocus,
                                                     newLocusOfFocus)
            if commonAncestor:
                context = self.speechGenerator.getSpeechContext( \
                                           newLocusOfFocus, commonAncestor)
                utterances.append(" ".join(context))

            # Now, we'll treat table row and column headers as context
            # as well.  This requires special handling because we're
            # making headers seem hierarchical in the context, but they
            # are not hierarchical in the containment hierarchicy.
            # We also only want to speak the one that changed.  If both
            # changed, first speak the row header, then the column header.
            #
            # We also keep track of tree level depth and only announce
            # that if it changes.
            #
            # Note that Java Swing allows things like ROLE_LABEL objects
            # in trees and tables, so we'll check the parent's role to 
            # see if it is a table.
            #
            oldNodeLevel = -1
            newNodeLevel = -1
            if (newLocusOfFocus.getRole() == pyatspi.ROLE_TABLE_CELL) \
               or (newParent.getRole() == pyatspi.ROLE_TABLE):
                try:
                    table = oldParent.queryTable()
                except:
                    table = None
                if table and \
                      ((oldLocusOfFocus.getRole() == pyatspi.ROLE_TABLE_CELL) \
                       or (oldParent.getRole() == pyatspi.ROLE_TABLE)):
                    index = self.getCellIndex(oldLocusOfFocus)
                    oldRow = table.getRowAtIndex(index)
                    oldCol = table.getColumnAtIndex(index)
                else:
                    oldRow = -1
                    oldCol = -1

                try:
                    table = newParent.queryTable()
                except:
                    pass
                else:
                    index = self.getCellIndex(newLocusOfFocus)
                    newRow = table.getRowAtIndex(index)
                    newCol = table.getColumnAtIndex(index)

                    if (newRow >= 0) \
                       and ((newRow != oldRow) or (oldParent != newParent)):
                        # Get the header information.  In Java Swing, the
                        # information is not exposed via the description
                        # but is instead a header object, so we fall back
                        # to that if it exists.
                        #
                        # [[[TODO: WDW - the more correct thing to do, I 
                        # think, is to look at the row header object.
                        # We've been looking at the description for so 
                        # long, though, that we'll give the description 
                        # preference for now.]]]
                        #
                        desc = table.getRowDescription(newRow)
                        if not desc:
                            header = table.getRowHeader(newRow)
                            if header:
                                desc = self.getDisplayedText(header)
                        if desc and len(desc):
                            text = desc
                            if settings.speechVerbosityLevel \
                                   == settings.VERBOSITY_LEVEL_VERBOSE:
                                text += " " \
                                        + rolenames.rolenames[\
                                        pyatspi.ROLE_ROW_HEADER].speech
                            utterances.append(text)
                    if (newCol >= 0) \
                       and ((newCol != oldCol) or (oldParent != newParent)):
                        # Don't speak Thunderbird column headers, since
                        # it's not possible to navigate across a row.
                        topName = self.getTopLevelName(newLocusOfFocus)
                        if not topName.endswith(" - Thunderbird"):
                            # Get the header information.  In Java Swing, the
                            # information is not exposed via the description
                            # but is instead a header object, so we fall back
                            # to that if it exists.
                            #
                            # [[[TODO: WDW - the more correct thing to do, I 
                            # think, is to look at the row header object.
                            # We've been looking at the description for so 
                            # long, though, that we'll give the description 
                            # preference for now.]]]
                            #
                            desc = table.getColumnDescription(newCol)
                            if not desc:
                                header = table.getColumnHeader(newCol)
                                if header:
                                    desc = self.getDisplayedText(header)
                            cellText = self.getDisplayedText(newLocusOfFocus)
                            if desc and len(desc) and cellText != desc:
                                text = desc
                                if settings.speechVerbosityLevel \
                                       == settings.VERBOSITY_LEVEL_VERBOSE:
                                    text += " " \
                                            + rolenames.rolenames[\
                                            pyatspi.ROLE_COLUMN_HEADER].speech
                                utterances.append(text)

            oldNodeLevel = self.getNodeLevel(oldLocusOfFocus)
            newNodeLevel = self.getNodeLevel(newLocusOfFocus)

            # We'll also treat radio button groups as though they are
            # in a context, with the label for the group being the
            # name of the context.
            #
            if newLocusOfFocus \
               and newLocusOfFocus.getRole() == pyatspi.ROLE_RADIO_BUTTON:
                radioGroupLabel = None
                inSameGroup = False
                relations = newLocusOfFocus.getRelationSet()
                for relation in relations:
                    if (not radioGroupLabel) \
                        and (relation.getRelationType() \
                             == pyatspi.RELATION_LABELLED_BY):
                        radioGroupLabel = relation.getTarget(0)
                    if (not inSameGroup) \
                        and (relation.getRelationType() \
                             == pyatspi.RELATION_MEMBER_OF):
                        for i in range(0, relation.getNTargets()):
                            target = relation.getTarget(i)
                            if target == oldLocusOfFocus:
                                inSameGroup = True
                                break

                # We'll only announce the radio button group when we
                # switch groups.
                #
                if (not inSameGroup) and radioGroupLabel:
                    utterances.append(self.getDisplayedText(radioGroupLabel))

            # Check to see if we are in the Pronunciation Dictionary in the
            # Orca Preferences dialog. If so, then we do not want to use the
            # pronunciation dictionary to replace the actual words in the
            # first column of this table.
            #
            rolesList = [pyatspi.ROLE_TABLE_CELL, \
                         pyatspi.ROLE_TABLE, \
                         pyatspi.ROLE_SCROLL_PANE, \
                         pyatspi.ROLE_PANEL, \
                         pyatspi.ROLE_PANEL]
            if self.isDesiredFocusedItem(newLocusOfFocus, rolesList) and \
               newLocusOfFocus.getApplication().name == "orca":
                orca_state.usePronunciationDictionary = False
            else:
                orca_state.usePronunciationDictionary = True

            # Get the text for the object itself.
            #
            utterances.extend(
                self.speechGenerator.getSpeech(newLocusOfFocus, False))
            utterances.extend(
                self.tutorialGenerator.getTutorial(newLocusOfFocus, False))
            # Now speak the new tree node level if it has changed.
            #
            if (oldNodeLevel != newNodeLevel) \
               and (newNodeLevel >= 0):
                # Translators: this represents the depth of a node in a tree
                # view (i.e., how many ancestors a node has).
                #
                utterances.append(_("tree level %d") % (newNodeLevel + 1))

            # If this is an icon within an layered pane or a table cell
            # within a table or a tree table and the item is focused but not
            # selected, let the user know. See bug #486908 for more details.
            #
            checkIfSelected = False
            objRole, parentRole, state = None, None, None
            if newLocusOfFocus:
                objRole = newLocusOfFocus.getRole()
                state = newLocusOfFocus.getState()
                if newLocusOfFocus.parent:
                    parentRole = newLocusOfFocus.parent.getRole()

            if objRole == pyatspi.ROLE_TABLE_CELL and \
               (parentRole == pyatspi.ROLE_TREE_TABLE or \
                parentRole == pyatspi.ROLE_TABLE):
                checkIfSelected = True

            # If we met the last set of conditions, but we got here by
            # moving left or right on the same row, then don't announce the
            # selection state to the user. See bug #523235 for more details.
            #
            if checkIfSelected == True and \
               (orca_state.lastNonModifierKeyEvent and \
               (orca_state.lastNonModifierKeyEvent.event_string == "Left" or \
               orca_state.lastNonModifierKeyEvent.event_string == "Right")):
                checkIfSelected = False

            if objRole == pyatspi.ROLE_ICON and \
                parentRole == pyatspi.ROLE_LAYERED_PANE:
                checkIfSelected = True

            if checkIfSelected and state \
               and not state.contains(pyatspi.STATE_SELECTED):
                # Translators: this is in reference to a table cell being
                # selected or not.
                #
                utterances.append(C_("tablecell", " not selected"))

            # We might be automatically speaking the unbound labels
            # in a dialog box as the result of the dialog box suddenly
            # appearing.  If so, don't interrupt this because of a
            # focus event that occurs when something like the "OK"
            # button gets focus shortly after the window appears.
            #
            shouldNotInterrupt = (event and event.type.startswith("focus:")) \
                and self.windowActivateTime \
                and ((time.time() - self.windowActivateTime) < 1.0)

            if objRole == pyatspi.ROLE_LINK:
                voice = self.voices[settings.HYPERLINK_VOICE]
            else:
                voice = self.voices[settings.DEFAULT_VOICE]

            speech.speakUtterances(utterances, voice, not shouldNotInterrupt)

            # If this is a table cell, save the current row and column
            # information in the table cell's table, so that we can use
            # it the next time.
            #
            if objRole == pyatspi.ROLE_TABLE_CELL:
                try:
                    table = newParent.queryTable()
                except:
                    pass
                else:
                    index = self.getCellIndex(newLocusOfFocus)
                    column = table.getColumnAtIndex(index)
                    self.pointOfReference['lastColumn'] = column
                    row = table.getRowAtIndex(index)
                    self.pointOfReference['lastRow'] = row
        else:
            orca_state.noFocusTimeStamp = time.time()

    def visualAppearanceChanged(self, event, obj):
        """Called when the visual appearance of an object changes.  This
        method should not be called for objects whose visual appearance
        changes solely because of focus -- setLocusOfFocus is used for that.
        Instead, it is intended mostly for objects whose notional 'value' has
        changed, such as a checkbox changing state, a progress bar advancing,
        a slider moving, text inserted, caret moved, etc.

        Arguments:
        - event: if not None, the Event that caused this to happen
        - obj: the Accessible whose visual appearance changed.
        """
        # Check if this event is for a progress bar.
        #
        if obj.getRole() == pyatspi.ROLE_PROGRESS_BAR:
            self.handleProgressBarUpdate(event, obj)

        if self.flatReviewContext:
            if self.isSameObject(
                obj,
                self.flatReviewContext.getCurrentAccessible()):
                self.updateBrailleReview()
            return

        # We care if panels are suddenly showing.  The reason for this
        # is that some applications, such as Evolution, will bring up
        # a wizard dialog that uses "Forward" and "Backward" buttons
        # that change the contents of the dialog.  We only discover
        # this through showing events. [[[TODO: WDW - perhaps what we
        # really want is to speak unbound labels that are suddenly
        # showing?  event.detail == 1 means object is showing.]]]
        #
        # [[[TODO: WDW - I added the 'False' condition to prevent this
        # condition from ever working.  I wanted to keep the code around,
        # though, just in case we want to reuse it somewhere else.  The
        # bug that spurred all of this on is:
        #
        #    http://bugzilla.gnome.org/show_bug.cgi?id=338687
        #
        # The main problem is that the profile editor in gnome-terminal
        # ended up being very verbose and speaking lots of things it
        # should not have been speaking.]]]
        #
        if False and (obj.getRole() == pyatspi.ROLE_PANEL) \
               and (event.detail1 == 1) \
               and self.isInActiveApp(obj):

            # It's only showing if its parent is showing. [[[TODO: WDW -
            # HACK we stop at the application level because applications
            # never seem to have their showing state set.]]]
            #
            reallyShowing = True
            parent = obj.parent
            while reallyShowing \
                      and parent \
                      and (parent != parent.parent) \
                      and (parent.getRole() != pyatspi.ROLE_APPLICATION):
                debug.println(debug.LEVEL_FINEST,
                              "default.visualAppearanceChanged - " \
                              + "checking parent")
                reallyShowing = parent.getState().contains( \
                                                  pyatspi.STATE_SHOWING)
                parent = parent.parent

            # Find all the unrelated labels in the dialog and speak them.
            #
            if reallyShowing:
                utterances = []
                labels = self.findUnrelatedLabels(obj)
                for label in labels:
                    utterances.append(label.name)

                speech.speakUtterances(utterances)

                return

        # If this object is CONTROLLED_BY the object that currently
        # has focus, speak/braille this object.
        #
        relations = obj.getRelationSet()
        for relation in relations:
            if relation.getRelationType() \
                   == pyatspi.RELATION_CONTROLLED_BY:
                target = relation.getTarget(0)
                if target == orca_state.locusOfFocus:
                    self.updateBraille(target)
                    utterances = self.speechGenerator.getSpeech(target, True)
                    utterances.extend(self.tutorialGenerator.getTutorial(
                               target, True))
                    speech.speakUtterances(utterances)
                    return

        # If this object is a label, and if it has a LABEL_FOR relation
        # to the focused object, then we should speak/braille the
        # focused object, as if it had just got focus.
        #
        if obj.getRole() == pyatspi.ROLE_LABEL \
           and obj.getState().contains(pyatspi.STATE_SHOWING):
            for relation in relations:
                if relation.getRelationType() \
                       == pyatspi.RELATION_LABEL_FOR:
                    target = relation.getTarget(0)
                    if target == orca_state.locusOfFocus:
                        self.updateBraille(target)
                        utterances = self.speechGenerator.getSpeech(
                                     target, True)
                        utterances.extend(self.tutorialGenerator.getTutorial(
                                          target, True))
                        speech.speakUtterances(utterances)
                        return

        if not self.isSameObject(obj, orca_state.locusOfFocus):
            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 so select them.  We see this
        # in the disk selection area of the OpenSolaris gui-install application
        # for example.
        #
        if obj.getRole() == pyatspi.ROLE_RADIO_BUTTON:
            if orca_state.lastNonModifierKeyEvent \
               and orca_state.lastNonModifierKeyEvent.event_string == "space":
                pass
            else:
                return

        if event:
            debug.println(debug.LEVEL_FINE,
                          "VISUAL CHANGE: '%s' '%s' (event='%s')" \
                          % (obj.name, obj.getRole(), event.type))
        else:
            debug.println(debug.LEVEL_FINE,
                          "VISUAL CHANGE: '%s' '%s' (event=None)" \
                          % (obj.name, obj.getRole()))

        mag.magnifyAccessible(event, obj)
        self.updateBraille(obj)
        utterances = self.speechGenerator.getSpeech(obj, True)
        utterances.extend(self.tutorialGenerator.getTutorial(obj, True))
        speech.speakUtterances(utterances)

    def updateBraille(self, obj, extraRegion=None):
        """Updates the braille display to show the give object.

        Arguments:
        - obj: the Accessible
        - extra: extra Region to add to the end
        """

        if not obj:
            return

        braille.clear()

        line = braille.Line()
        braille.addLine(line)

        # For multiline text areas, we only show the context if we
        # are on the very first line.  Otherwise, we show only the
        # line.
        #
        try:
            text = obj.queryText()
        except NotImplementedError:
            text = None
        if text and self.isTextArea(obj):
            [lineString, startOffset, endOffset] = text.getTextAtOffset(
                text.caretOffset,
                pyatspi.TEXT_BOUNDARY_LINE_START)
            if startOffset == 0:
                line.addRegions(self.brailleGenerator.getBrailleContext(obj))
        else:
            line.addRegions(self.brailleGenerator.getBrailleContext(obj))

        result = self.brailleGenerator.getBrailleRegions(obj)
        line.addRegions(result[0])

        if extraRegion:
            line.addRegion(extraRegion)

        if extraRegion:
            braille.setFocus(extraRegion)
        else:
            braille.setFocus(result[1])

        braille.refresh(True)

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

    def onFocus(self, event):
        """Called whenever an object gets focus.

        Arguments:
        - event: the Event
        """

        # [[[TODO: WDW - HACK to deal with quirky GTK+ menu behavior.
        # The problem is that when moving to submenus in a menu, the
        # menu gets focus first and then the submenu gets focus all
        # with a single keystroke.  So...focus in menus really means
        # that the object has focus *and* it is selected.  Now, this
        # assumes the selected state will be set before focus is given,
        # which appears to be the case from empirical analysis of the
        # event stream.  But of course, all menu items and menus in
        # the complete menu path will have their selected state set,
        # so, we really only care about the leaf menu or menu item
        # that it selected.]]]
        #
        role = event.source.getRole()
        if role in (pyatspi.ROLE_MENU,
                    pyatspi.ROLE_MENU_ITEM,
                    pyatspi.ROLE_CHECK_MENU_ITEM,
                    pyatspi.ROLE_RADIO_MENU_ITEM):
            try:
                if event.source.querySelection().nSelectedChildren > 0:
                    return
            except:
                pass

        # [[[TODO: WDW - HACK to deal with the fact that active cells
        # may or may not get focus.  Their parents, however, do tend to
        # get focus, but when the parent gets focus, it really means
        # that the selected child in it has focus.  Of course, this all
        # breaks when more than one child is selected.  Then, we really
        # need to depend upon the model where focus really works.]]]
        #
        newFocus = event.source

        if role in (pyatspi.ROLE_LAYERED_PANE,
                    pyatspi.ROLE_TABLE,
                    pyatspi.ROLE_TREE_TABLE,
                    pyatspi.ROLE_TREE):
            if event.source.childCount:
                # We might have tucked away some information for this
                # thing in the onActiveDescendantChanged method.
                #
                if "activeDescendantInfo" in self.pointOfReference:
                    [parent, index] = \
                        self.pointOfReference['activeDescendantInfo']
                    newFocus = parent[index]

                else:
                    # Well...we'll first see if there is a selection.  If there
                    # is, we'll use it.
                    #
                    try:
                        selection = event.source.querySelection()
                    except NotImplementedError:
                        selection = None
                    if selection and selection.nSelectedChildren > 0:
                        newFocus = selection.getSelectedChild(0)

        orca.setLocusOfFocus(event, newFocus)

    def onNameChanged(self, event):
        """Called whenever a property on an object changes.

        Arguments:
        - event: the Event
        """

        # [[[TODO: WDW - HACK because gnome-terminal issues a name changed
        # event for the edit preferences dialog even though the name really
        # didn't change.  I'm guessing this is going to be a vagary in all
        # of GTK+.]]]
        #
        if event.source and (event.source.getRole() == pyatspi.ROLE_DIALOG) \
           and (event.source == orca_state.locusOfFocus):
            return

        # We do this because we can get name change events even if the
        # name doesn't change.  [[[TODO: WDW - I'm hesitant to rip the
        # above TODO out, though, because it's been in here for so long.]]]
        #
        if self.pointOfReference.get('oldName', None) == event.source.name:
            return

        self.pointOfReference['oldName'] = event.source.name
        orca.visualAppearanceChanged(event, event.source)

    def _speakContiguousSelection(self, obj, relationship):
        """Check if the contiguous object has a selection. If it does, then
        speak it. If the user pressed Shift-Down, then look for an object 
        with a RELATION_FLOWS_FROM relationship. If they pressed Shift-Up,
        then look for a RELATION_FLOWS_TO relationship.

        Arguments:
        - the current text object
        - the flows relationship (RELATION_FLOWS_FROM or RELATION_FLOWS_TO).

        Returns an indication of whether anything was spoken.
        """

        lastPos = self.pointOfReference.get("lastCursorPosition")

        # Reasons to NOT speak contiguous selections:
        #
        # 1. The new cursor position is in the same object as the old
        #    cursor position. (The change in selection is all within
        #    the current object.)
        # 2. If we are selecting up line by line from the beginning of
        #    the line and have just crossed into a new object, the change
        #    in selection is the previous line (which has just become
        #    selected).  Nothing has changed on the line we came from.
        #
        if self.isSameObject(lastPos[0], obj) \
           or relationship == pyatspi.RELATION_FLOWS_TO and lastPos[1] == 0:
            return False

        selSpoken = False
        current = obj
        for relation in current.getRelationSet():
            if relation.getRelationType() == relationship:
                obj = relation.getTarget(0)
                objText = obj.queryText()

                # When selecting down across paragraph boundaries, what
                # we've (un)selected on (what is now) the previous line
                # is from wherever the cursor used to be to the end of 
                # the line.
                #
                if relationship == pyatspi.RELATION_FLOWS_FROM:
                    start, end = lastPos[1], objText.characterCount

                # When selecting up across paragraph boundaries, what
                # we've (un)selected on (what is now) the next line is
                # from the beginning of the line to wherever the cursor
                # used to be.
                #
                else:
                    start, end = 0, lastPos[1]

                if objText.getNSelections() > 0:
                    [textContents, startOffset, endOffset] = \
                        self.whereAmI.getTextSelection(obj)

                    # Now that we have the full selection, adjust based
                    # on the relation type. (see above comment)
                    #
                    startOffset = start or startOffset
                    endOffset = end or endOffset
                    self.sayPhrase(obj, startOffset, endOffset)
                    selSpoken = True
                else:
                    # We don't have selections in this object. But we're
                    # here, which means that something is selected in a
                    # neighboring object and the text in this object must
                    # have just become unselected and needs to be spoken.
                    #
                    self.sayPhrase(obj, start, end)
                    selSpoken = True

        return selSpoken

    def _presentTextAtNewCaretPosition(self, event, otherObj=None):
        """Updates braille, magnification, and outputs speech for the 
        event.source or the otherObj."""

        obj = otherObj or event.source
        text = obj.queryText()

        if obj:
            mag.magnifyAccessible(event, obj)

        # Update the Braille display - if we can just reposition
        # the cursor, then go for it.
        #
        brailleNeedsRepainting = True
        line = braille.getShowingLine()
        for region in line.regions:
            if isinstance(region, braille.Text) \
               and (region.accessible == obj):
                if region.repositionCursor():
                    braille.refresh(True)
                    brailleNeedsRepainting = False
                break

        if brailleNeedsRepainting:
            self.updateBraille(obj)

        if not orca_state.lastInputEvent:
            return

        if isinstance(orca_state.lastInputEvent, input_event.MouseButtonEvent):
            if not orca_state.lastInputEvent.pressed:
                self.sayLine(obj)
            return

        # Guess why the caret moved and say something appropriate.
        # [[[TODO: WDW - this motion assumes traditional GUI
        # navigation gestures.  In an editor such as vi, line up and
        # down is done via other actions such as "i" or "j".  We may
        # need to think about this a little harder.]]]
        #
        if not isinstance(orca_state.lastInputEvent,
                          input_event.KeyboardEvent):
            return

        keyString = orca_state.lastNonModifierKeyEvent.event_string
        mods = orca_state.lastInputEvent.modifiers
        isControlKey = mods & settings.CTRL_MODIFIER_MASK
        isShiftKey = mods & settings.SHIFT_MODIFIER_MASK
        lastPos = self.pointOfReference.get("lastCursorPosition")
        hasLastPos = (lastPos != None)

        if (keyString == "Up") or (keyString == "Down"):
            # If the user has typed Shift-Up or Shift-Down, then we want
            # to speak the text that has just been selected or unselected,
            # otherwise we speak the new line where the text cursor is
            # currently positioned.
            #
            if hasLastPos and isShiftKey and not isControlKey:
                if keyString == "Up":
                    # If we have just crossed a paragraph boundary with
                    # Shift+Up, what we've selected in this object starts
                    # with the current offset and goes to the end of the
                    # paragraph.
                    # 
                    if not self.isSameObject(lastPos[0], obj):
                        [startOffset, endOffset] = \
                            text.caretOffset, text.characterCount
                    else:
                        [startOffset, endOffset] \
                                             = self.getOffsetsForPhrase(obj)
                    self.sayPhrase(obj, startOffset, endOffset)
                    selSpoken = self._speakContiguousSelection(obj,
                                                   pyatspi.RELATION_FLOWS_TO)
                else:
                    selSpoken = self._speakContiguousSelection(obj,
                                                 pyatspi.RELATION_FLOWS_FROM)

                    # If we have just crossed a paragraph boundary with
                    # Shift+Down, what we've selected in this object starts
                    # with the beginning of the paragraph and goes to the
                    # current offset.
                    #
                    if not self.isSameObject(lastPos[0], obj):
                        [startOffset, endOffset] = 0, text.caretOffset
                    else:
                        [startOffset, endOffset] \
                                             = self.getOffsetsForPhrase(obj)

                    if startOffset != endOffset:
                        self.sayPhrase(obj, startOffset, endOffset)

            else:
                [startOffset, endOffset] = self.getOffsetsForLine(obj)
                self.sayLine(obj)

        elif (keyString == "Left") or (keyString == "Right"):
            # If the user has typed Control-Shift-Up or Control-Shift-Dowm,
            # then we want to speak the text that has just been selected
            # or unselected, otherwise if the user has typed Control-Left
            # or Control-Right, we speak the current word otherwise we speak
            # the character at the text cursor position.
            #
            inNewObj = hasLastPos and not self.isSameObject(lastPos[0], obj)

            if hasLastPos and not inNewObj and isShiftKey and isControlKey:
                [startOffset, endOffset] = self.getOffsetsForPhrase(obj)
                self.sayPhrase(obj, startOffset, endOffset)
            elif isControlKey and not inNewObj:
                [startOffset, endOffset] = self.getOffsetsForWord(obj)
                if startOffset == endOffset:
                    self.sayCharacter(obj)
                else:
                    self.sayWord(obj)
            else:
                [startOffset, endOffset] = self.getOffsetsForChar(obj)
                self.sayCharacter(obj)

        elif keyString == "Page_Up":
            # If the user has typed Control-Shift-Page_Up, then we want
            # to speak the text that has just been selected or unselected,
            # otherwise if the user has typed Control-Page_Up, then we
            # speak the character to the right of the current text cursor
            # position otherwise we speak the current line.
            #
            if hasLastPos and isShiftKey and isControlKey:
                [startOffset, endOffset] = self.getOffsetsForPhrase(obj)
                self.sayPhrase(obj, startOffset, endOffset)
            elif isControlKey:
                [startOffset, endOffset] = self.getOffsetsForChar(obj)
                self.sayCharacter(obj)
            else:
                [startOffset, endOffset] = self.getOffsetsForLine(obj)
                self.sayLine(obj)

        elif keyString == "Page_Down":
            # If the user has typed Control-Shift-Page_Down, then we want
            # to speak the text that has just been selected or unselected,
            # otherwise if the user has just typed Page_Down, then we speak
            # the current line.
            #
            if hasLastPos and isShiftKey and isControlKey:
                [startOffset, endOffset] = self.getOffsetsForPhrase(obj)
                self.sayPhrase(obj, startOffset, endOffset)
            else:
                [startOffset, endOffset] = self.getOffsetsForLine(obj)
                self.sayLine(obj)

        elif (keyString == "Home") or (keyString == "End"):
            # If the user has typed Shift-Home or Shift-End, then we want
            # to speak the text that has just been selected or unselected,
            # otherwise if the user has typed Control-Home or Control-End,
            # then we speak the current line otherwise we speak the character
            # to the right of the current text cursor position.
            #
            if hasLastPos and isShiftKey and not isControlKey:
                [startOffset, endOffset] = self.getOffsetsForPhrase(obj)
                self.sayPhrase(obj, startOffset, endOffset)
            elif isControlKey:
                [startOffset, endOffset] = self.getOffsetsForLine(obj)
                self.sayLine(obj)
            else:
                [startOffset, endOffset] = self.getOffsetsForChar(obj)
                self.sayCharacter(obj)

        else:
            startOffset = text.caretOffset
            endOffset = text.caretOffset

        self._saveLastCursorPosition(obj, text.caretOffset)
        self._saveSpokenTextRange(startOffset, endOffset)

    def onCaretMoved(self, event):
        """Called whenever the caret moves.

        Arguments:
        - event: the Event
        """

        # Ignore caret movements from non-focused objects, unless the
        # currently focused object is the parent of the object which
        # has the caret.
        #
        if (event.source != orca_state.locusOfFocus) \
            and (event.source.parent != orca_state.locusOfFocus):
            return

        # We always automatically go back to focus tracking mode when
        # the caret moves in the focused object.
        #
        if self.flatReviewContext:
            self.toggleFlatReviewMode()

        self._presentTextAtNewCaretPosition(event)

    def onTextDeleted(self, event):
        """Called whenever text is deleted from an object.

        Arguments:
        - event: the Event
        """

        # Ignore text deletions from non-focused objects, unless the
        # currently focused object is the parent of the object from which
        # text was deleted
        #
        if (event.source != orca_state.locusOfFocus) \
            and (event.source.parent != orca_state.locusOfFocus):
            return

        # We'll also ignore sliders because we get their output via
        # their values changing.
        #
        if event.source.getRole() == pyatspi.ROLE_SLIDER:
            return

        # [[[NOTE: WDW - if we handle events synchronously, we'll
        # be looking at the text object *before* the text was
        # actually removed from the object.  If we handle events
        # asynchronously, we'll be looking at the text object
        # *after* the text was removed.  The importance of knowing
        # this is that the output will differ depending upon how
        # orca.settings.asyncMode has been set.  For example, the
        # regression tests run in synchronous mode, so the output
        # they see will not be the same as what the user normally
        # experiences.]]]

        self.updateBraille(event.source)

        # The any_data member of the event object has the deleted text in
        # it - If the last key pressed was a backspace or delete key,
        # speak the deleted text.  [[[TODO: WDW - again, need to think
        # about the ramifications of this when it comes to editors such
        # as vi or emacs.
        #
        if (not orca_state.lastInputEvent) \
            or \
            (not isinstance(orca_state.lastInputEvent,
                            input_event.KeyboardEvent)):
            return

        keyString = orca_state.lastNonModifierKeyEvent.event_string
        controlPressed = orca_state.lastInputEvent.modifiers \
                         & settings.CTRL_MODIFIER_MASK
        text = event.source.queryText()
        if keyString == "BackSpace":
            # Speak the character that has just been deleted.
            #
            character = event.any_data

        elif (keyString == "Delete") \
             or (keyString == "D" and controlPressed):
            # Speak the character to the right of the caret after
            # the current right character has been deleted.
            #
            offset = text.caretOffset
            [character, startOffset, endOffset] = \
                text.getTextAtOffset(
                    offset,
                    pyatspi.TEXT_BOUNDARY_CHAR)

        else:
            return

        if self.getLinkIndex(event.source, text.caretOffset) >= 0:
            voice = self.voices[settings.HYPERLINK_VOICE]
        elif character.isupper():
            voice = self.voices[settings.UPPERCASE_VOICE]
        else:
            voice = self.voices[settings.DEFAULT_VOICE]

        # We won't interrupt what else might be being spoken
        # right now because it is typically something else
        # related to this event.
        #
        if len(character.decode('utf-8')) == 1:
            speech.speakCharacter(character, voice)
        else:
            speech.speak(character, voice, False)

    def onTextInserted(self, event):
        """Called whenever text is inserted into an object.

        Arguments:
        - event: the Event
        """

        # Ignore text insertions from non-focused objects, unless the
        # currently focused object is the parent of the object from which
        # text was inserted.
        #
        if (event.source != orca_state.locusOfFocus) \
            and (event.source.parent != orca_state.locusOfFocus):
            return

        # We'll also ignore sliders because we get their output via
        # their values changing.
        #
        if event.source.getRole() == pyatspi.ROLE_SLIDER:
            return

        self.updateBraille(event.source)

        text = event.any_data

        # If this is a spin button, then speak the text and return.
        #
        if event.source.getRole() == pyatspi.ROLE_SPIN_BUTTON:
            # We are using getTextLineAtCaret here instead of the "text"
            # variable, because of a problem with selected text in spin
            # buttons. See bug #520395 for more details.
            #
            [spinValue, caretOffset, startOffset] = \
                self.getTextLineAtCaret(event.source)
            speech.speak(spinValue)
            return

        # If the last input event was a keyboard event, check to see if
        # the text for this event matches what the user typed. If it does,
        # then don't speak it.
        #
        # Note that the text widgets sometimes compress their events,
        # thus we might get a longer string from a single text inserted
        # event, while we also get individual keyboard events for the
        # characters used to type the string.  This is ugly.  We attempt
        # to handle it here by only echoing text if we think it was the
        # result of a command (e.g., a paste operation).
        #
        # Note that we have to special case the space character as it
        # comes across as "space" in the keyboard event and " " in the
        # text event.
        #
        speakThis = False
        if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
            keyString = orca_state.lastNonModifierKeyEvent.event_string
            wasAutoComplete = (event.source.getRole() == pyatspi.ROLE_TEXT and \
                               event.source.queryText().getNSelections())
            wasCommand = orca_state.lastInputEvent.modifiers \
                         & settings.COMMAND_MODIFIER_MASK
            if (text == " " and keyString == "space") \
                or (text == keyString):
                pass
            elif wasCommand or wasAutoComplete:
                speakThis = True
            elif (event.source.getRole() == pyatspi.ROLE_PASSWORD_TEXT) and \
                 settings.enableKeyEcho and settings.enablePrintableKeys:
                # Echoing "star" is preferable to echoing the descriptive
                # name of the bullet that has appeared (e.g. "black circle")
                #
                text = "*"
                speakThis = True

        elif isinstance(orca_state.lastInputEvent, \
                        input_event.MouseButtonEvent) and \
             orca_state.lastInputEvent.button == "2":
            speakThis = True

        if speakThis:
            if text.isupper():
                speech.speak(text, self.voices[settings.UPPERCASE_VOICE])
            else:
                speech.speak(text)

        try:
            text = event.source.queryText()
        except NotImplementedError:
            return

        # Pylint is confused and flags this and similar lines, with the 
        # following error:
        #
        # E1103:3673:Script.onTextInserted: Instance of 'str' has no 
        #'caretOffset' member (but some types could not be inferred)
        #
        # But it does, so we'll just tell pylint that we know what we
        # are doing.
        #
        # pylint: disable-msg=E1103

        offset = min(event.detail1, text.caretOffset - 1)
        previousOffset = offset - 1
        if (offset < 0 or previousOffset < 0):
            return

        [currentChar, startOffset, endOffset] = \
            text.getTextAtOffset(offset, pyatspi.TEXT_BOUNDARY_CHAR)
        [previousChar, startOffset, endOffset] = \
            text.getTextAtOffset(previousOffset, pyatspi.TEXT_BOUNDARY_CHAR)

        if settings.enableEchoBySentence and \
           self.isSentenceDelimiter(currentChar, previousChar):
            self.echoPreviousSentence(event.source)

        elif settings.enableEchoByWord and self.isWordDelimiter(currentChar):
            self.echoPreviousWord(event.source)

    def onActiveDescendantChanged(self, event):
        """Called when an object who manages its own descendants detects a
        change in one of its children.

        Arguments:
        - event: the Event
        """

        if not event.source.getState().contains(pyatspi.STATE_FOCUSED):
            return

        # There can be cases when the object that fires an
        # active-descendant-changed event has no children. In this case,
        # use the object that fired the event, otherwise, use the child.
        #
        child = event.any_data
        if child:
            speech.stop()
            orca.setLocusOfFocus(event, child)
        else:
            orca.setLocusOfFocus(event, event.source)

        # We'll tuck away the activeDescendant information for future
        # reference since the AT-SPI gives us little help in finding
        # this.
        #
        if orca_state.locusOfFocus \
           and (orca_state.locusOfFocus != event.source):
            self.pointOfReference['activeDescendantInfo'] = \
                [orca_state.locusOfFocus.parent,
                 orca_state.locusOfFocus.getIndexInParent()]

    def onLinkSelected(self, event):
        """Called when a hyperlink is selected in a text area.

        Arguments:
        - event: the Event
        """

        # [[[TODO: WDW - HACK one might think we could expect an
        # application to keep its name, but it appears as though
        # yelp has an identity problem and likes to start calling
        # itself "yelp," but then changes its name to "Mozilla"
        # on Fedora Core 4 after the user selects a link.  So, we'll
        # just assume that link-selected events always come from the
        # application with focus.]]]
        #
        #if orca_state.locusOfFocus \
        #   and (orca_state.locusOfFocus.app == event.source.app):
        #    orca.setLocusOfFocus(event, event.source)
        orca.setLocusOfFocus(event, event.source)

    def onStateChanged(self, event):
        """Called whenever an object's state changes.

        Arguments:
        - event: the Event
        """

        # Do we care?
        #
        if event.type.startswith("object:state-changed:active"):
            if self.findCommandRun:
                self.findCommandRun = False
                self.find()
                return

        if event.type.startswith("object:state-changed:selected") \
           and orca_state.locusOfFocus:
            # If this selection state change is for the object which
            # currently has the locus of focus, and the last keyboard
            # event was Space, or we are a focused table cell and we
            # arrowed Down or Up and are now selected, then let the
            # user know the selection state.
            # See bugs #486908 and #519564 for more details.
            #
            if isinstance(orca_state.lastInputEvent,
                          input_event.KeyboardEvent):
                if orca_state.lastNonModifierKeyEvent:
                    keyString = orca_state.lastNonModifierKeyEvent.event_string
                else:
                    keyString = None
                mods = orca_state.lastInputEvent.modifiers
                isControlKey = mods & settings.CTRL_MODIFIER_MASK
                state = orca_state.locusOfFocus.getState()
                announceState = False

                if state.contains(pyatspi.STATE_FOCUSED) and \
                   self.isSameObject(event.source, orca_state.locusOfFocus):

                    if keyString == "space":
                        if isControlKey:
                            announceState = True
                        else:
                            # Weed out a bogus situation. If we are already
                            # selected and the user presses "space" again,
                            # we don't want to speak the intermediate
                            # "unselected" state.
                            #
                            eventState = event.source.getState()
                            selected = eventState.contains(\
                                           pyatspi.STATE_SELECTED)
                            announceState = (selected and event.detail1)

                    if (keyString == "Down" or keyString == "Up") \
                       and event.source.getRole() == pyatspi.ROLE_TABLE_CELL \
                       and state.contains(pyatspi.STATE_SELECTED):
                        announceState = True

                if announceState:
                    if event.detail1:
                        # Translators: this object is now selected.
                        # Let the user know this.
                        #
                        #
                        speech.speak(C_("text", "selected"), None, False)
                    else:
                        # Translators: this object is now unselected.
                        # Let the user know this.
                        #
                        #
                        speech.speak(C_("text", "unselected"), None, False)
                    return

        if event.type.startswith("object:state-changed:focused"):
            iconified = False
            try:
                window = self.getTopLevel(event.source)
                iconified = window.getState().contains(pyatspi.STATE_ICONIFIED)
            except:
                debug.println(debug.LEVEL_FINEST,
                        "onStateChanged: could not get frame of focused item")
            if not iconified:
                if event.detail1:
                    self.onFocus(event)
                # We don't set locus of focus of None here because it
                # wreaks havoc on the code that determines the context
                # when you tab from widget to widget.  For example,
                # tabbing between panels in the gtk-demo buttons demo.
                #
                #else:
                #    orca.setLocusOfFocus(event, None)
                return

        # Handle tooltip popups.
        #
        if event.source.getRole() == pyatspi.ROLE_TOOL_TIP:
            obj = event.source

            if event.type.startswith("object:state-changed:showing"):
                if event.detail1 == 1:
                    self.presentTooltip(obj)
                elif orca_state.locusOfFocus \
                    and isinstance(orca_state.lastInputEvent,
                                   input_event.KeyboardEvent) \
                    and (orca_state.lastNonModifierKeyEvent.event_string \
                                                                  == "F1"):
                    self.updateBraille(orca_state.locusOfFocus)
                    utterances = self.speechGenerator.getSpeech(\
                        orca_state.locusOfFocus,
                        False)
                    utterances.extend(self.tutorialGenerator.getTutorial(
                                      orca_state.locusOfFocus, False))
                    speech.speakUtterances(utterances)
            return

        if event.source.getRole() in state_change_notifiers:
            notifiers = state_change_notifiers[event.source.getRole()]
            found = False
            for state in notifiers:
                if state and event.type.endswith(state):
                    found = True
                    break
            if found:
                orca.visualAppearanceChanged(event, event.source)

        # [[[TODO: WDW - HACK we'll handle this in the visual appearance
        # changed handler.]]]
        #
        # The object with focus might become insensitive, so we need to
        # flag that.  This typically occurs in wizard dialogs such as
        # the account setup assistant in Evolution.
        #
        #if event.type.endswith("sensitive") \
        #   and (event.detail1 == 0) \
        #   and event.source == orca_state.locusOfFocus:
        #    print "FOO INSENSITIVE"
        #    #orca.setLocusOfFocus(event, None)

    def getOffsetsForPhrase(self, obj):
        """Return the start and end offset for the given phrase

        Arguments:
        - obj: the Accessible object
        """

        text = obj.queryText()
        lastPos = self.pointOfReference.get("lastCursorPosition")
        startOffset = lastPos[1]
        endOffset = text.caretOffset

        # Swap values if in wrong order (StarOffice is fussy about that).
        #
        if ((startOffset > endOffset) and (endOffset != -1)) or \
           (startOffset == -1):
            temp = endOffset
            endOffset = startOffset
            startOffset = temp
        return [startOffset, endOffset]

    def getOffsetsForLine(self, obj):
        """Return the start and end offset for the given line

        Arguments:
        - obj: the Accessible object
        """

        [line, endOffset, startOffset] = self.getTextLineAtCaret(obj)
        return [startOffset, endOffset]

    def getOffsetsForWord(self, obj):
        """Return the start and end offset for the given word

        Arguments:
        - obj: the Accessible object
        """

        text = obj.queryText()
        offset = text.caretOffset
        [word, startOffset, endOffset] = text.getTextAtOffset(offset,
                                           pyatspi.TEXT_BOUNDARY_WORD_START)
        return [startOffset, endOffset]

    def getOffsetsForChar(self, obj):
        """Return the start and end offset for the given character

        Arguments:
        - obj: the Accessible object
        """

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

        mods = orca_state.lastInputEvent.modifiers
        if (mods & settings.SHIFT_MODIFIER_MASK) \
            and orca_state.lastNonModifierKeyEvent.event_string == "Right":
            startOffset = offset-1
            endOffset = offset
        else:
            startOffset = offset
            endOffset = offset+1

        return [startOffset, endOffset]

    def onTextSelectionChanged(self, event):
        """Called when an object's text selection changes.

        Arguments:
        - event: the Event
        """

        obj = event.source
        spokenRange = self.pointOfReference.get("spokenTextRange") or [0, 0]
        startOffset, endOffset = spokenRange

        if not obj.getState().contains(pyatspi.STATE_FOCUSED):
            # We're selecting across paragraph (or other text object)
            # boundaries. If we're here, the selection has changed in
            # an object which does not have the caret. We need to try
            # to sort this out.
            #
            lastPos = self.pointOfReference.get("lastCursorPosition")
            if not lastPos:
                # We have no point of reference. Bail.
                #
                return
            elif endOffset - startOffset > 1:
                # We're coming at the line from below. And didn't just
                # land on a blank/empty line. We have other methods for
                # dealing with this situation.
                #
                return

            # We must be approaching from the top, left, or right. Or
            # from below but we've found a blank line. Our stored point
            # of reference tells us our caret location. Figure out how
            # we got here by looking at our position with respect to
            # the event under consideration.
            #
            relationType = None
            for relation in lastPos[0].getRelationSet():
                if relation.getRelationType() in [pyatspi.RELATION_FLOWS_FROM,
                                                  pyatspi.RELATION_FLOWS_TO] \
                   and self.isSameObject(obj, relation.getTarget(0)):
                    relationType = relation.getRelationType()
                    break

            # If there's a completely blank line in between our previous
            # and current locations, where we came from will lack any
            # offically-selectable characters. As a result, we won't 
            # indicate when a blank line has been selected. Under these
            # conditions, we'll try to backtrack further.
            #
            endOffset = 0
            while obj and not endOffset:
                try:
                    endOffset = obj.queryText().characterCount
                    startOffset = max(0, endOffset - 1)
                except:
                    pass

                if not endOffset:
                    for relation in obj.getRelationSet():
                        if relation.getRelationType() == relationType:
                            obj = relation.getTarget(0)
                            break
                    else:
                        break

        self.speakTextSelectionState(obj, startOffset, endOffset)    
            
    def onSelectionChanged(self, event):
        """Called when an object's selection changes.

        Arguments:
        - event: the Event
        """

        if not event or not event.source:
            return

        # Save the event source, if it is a menu or combo box. It will be
        # useful for optimizing getComponentAtDesktopCoords in the case
        # that the  pointer is hovering over a menu item. The alternative is
        # to traverse the application's tree looking for potential moused-over
        # menu items.
        if event.source.getRole() in (pyatspi.ROLE_COMBO_BOX,
                                           pyatspi.ROLE_MENU):
            self.lastSelectedMenu = event.source

        # Avoid doing this with objects that manage their descendants
        # because they'll issue a descendant changed event.
        #
        if event.source.getState().contains(pyatspi.STATE_MANAGES_DESCENDANTS):
            return

        if event.source.getRole() == pyatspi.ROLE_COMBO_BOX:
            orca.visualAppearanceChanged(event, event.source)

        # We treat selected children as the locus of focus. When the
        # selection changed we want to update the locus of focus. If
        # there is no selection, we default the locus of focus to the
        # containing object.
        #
        elif (event.source != orca_state.locusOfFocus) and \
            event.source.getState().contains(pyatspi.STATE_FOCUSED):
            newFocus = event.source
            if event.source.childCount:
                selection = event.source.querySelection()
                if selection.nSelectedChildren > 0:
                    newFocus = selection.getSelectedChild(0)

            orca.setLocusOfFocus(event, newFocus)

    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
        """

        # We'll let caret moved and text inserted events be used to
        # manage spin buttons, since they basically are text areas.
        #
        if event.source.getRole() == pyatspi.ROLE_SPIN_BUTTON:
            return

        # We'll also try to ignore those objects that keep telling
        # us their value changed even though it hasn't.
        #
        value = event.source.queryValue()
        if "oldValue" in self.pointOfReference \
           and (value.currentValue == self.pointOfReference["oldValue"]):
            return

        orca.visualAppearanceChanged(event, event.source)
        if event.source.getState().contains(pyatspi.STATE_FOCUSED):
            self.pointOfReference["oldValue"] = value.currentValue

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

        Arguments:
        - event: the Event
        """

        self.windowActivateTime = time.time()
        orca.setLocusOfFocus(event, event.source)

        # We keep track of the active window to handle situations where
        # we get window activated and window deactivated events out of
        # order (see onWindowDeactivated).
        #
        # For example, events can be:
        #
        #    window:activate   (w1)
        #    window:activate   (w2)
        #    window:deactivate (w1)
        #
        # as well as:
        #
        #    window:activate   (w1)
        #    window:deactivate (w1)
        #    window:activate   (w2)
        #
        orca_state.activeWindow = event.source

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

        Arguments:
        - event: the Event
        """

        # If we receive a "window:deactivate" event for the object that
        # currently has focus, then stop the current speech output.
        # This is very useful for terminating long speech output from
        # commands running in gnome-terminal.
        #
        if orca_state.locusOfFocus and \
          (orca_state.locusOfFocus.getApplication() == \
             event.source.getApplication()):
            speech.stop()

            # Clear the braille display just in case we are about to give
            # focus to an inaccessible application. See bug #519901 for
            # more details.
            #
            braille.clear()

            # Hide the flat review window and reset it so that it will be
            # recreated.
            #
            if self.flatReviewContext:
                self.drawOutline(-1, 0, 0, 0)
                self.flatReviewContext = None
                self.updateBraille(orca_state.locusOfFocus)

        # Because window activated and deactivated events may be
        # received in any order when switching from one application to
        # another, locusOfFocus and activeWindow, we really only change
        # the locusOfFocus and activeWindow when we are dealing with
        # an event from the current activeWindow.
        #
        if event.source == orca_state.activeWindow:
            orca.setLocusOfFocus(event, None)
            orca_state.activeWindow = None

    def onMouseButton(self, event):
        """Called whenever the user presses or releases a mouse button.

        Arguments:
        - event: the Event
        """

        # If we've received a mouse button released event, then check if
        # there are and text selections for the locus of focus and speak
        # them.
        #
        state = event.type[-1]
        if state == "r":
            obj = orca_state.locusOfFocus
            try:
                text = obj.queryText()
            except:
                pass
            else:
                [textContents, startOffset, endOffset] = \
                    self.whereAmI.getTextSelections(obj, True)
                if textContents:
                    utterances = []
                    utterances.append(textContents)

                    # Translators: when the user selects (highlights) text in
                    # a document, Orca lets them know this.
                    #
                    utterances.append(C_("text", "selected"))
                    speech.speakUtterances(utterances)
                self.updateBraille(orca_state.locusOfFocus)

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

        Arguments:
        - event: the Event
        """
        pass

    ########################################################################
    #                                                                      #
    # Utilities                                                            #
    #                                                                      #
    ########################################################################

    def isLayoutOnly(self, obj):
        """Returns True if the given object is a table and is for layout
        purposes only."""

        layoutOnly = False

        if obj:
            attributes = obj.getAttributes()
        else:
            attributes = None

        if obj and (obj.getRole() == pyatspi.ROLE_TABLE) and attributes:
            for attribute in attributes:
                if attribute == "layout-guess:true":
                    layoutOnly = True
                    break
        elif obj and (obj.getRole() == pyatspi.ROLE_PANEL):
            text = self.getDisplayedText(obj)
            label = self.getDisplayedLabel(obj)
            if not ((label and len(label)) or (text and len(text))):
                layoutOnly = True

        if layoutOnly:
            debug.println(debug.LEVEL_FINEST,
                          "Object deemed to be for layout purposes only: %s" \
                          % obj)

        return layoutOnly

    def toggleTableCellReadMode(self, inputEvent=None):
        """Toggles an indicator for whether we should just read the current
        table cell or read the whole row."""

        settings.readTableCellRow = not settings.readTableCellRow
        if settings.readTableCellRow:
            # Translators: when users are navigating a table, they
            # sometimes want the entire row of a table read, or
            # they just want the current cell to be presented to them.
            #
            line = _("Speak row")
        else:
            # Translators: when users are navigating a table, they
            # sometimes want the entire row of a table read, or
            # they just want the current cell to be presented to them.
            #
            line = _("Speak cell")

        speech.speak(line)

        return True

    def getAtkNameForAttribute(self, attribName):
        """Converts the given attribute name into the Atk equivalent. This
        is necessary because an application or toolkit (e.g. Gecko) might
        invent entirely new names for the same attributes.

        Arguments:
        - attribName: The name of the text attribute

        Returns the Atk equivalent name if found or attribName otherwise.
        """

        return self.attributeNamesDict.get(attribName, attribName)

    def getAppNameForAttribute(self, attribName):
        """Converts the given Atk attribute name into the application's
        equivalent. This is necessary because an application or toolkit
        (e.g. Gecko) might invent entirely new names for the same text
        attributes.

        Arguments:
        - attribName: The name of the text attribute

        Returns the application's equivalent name if found or attribName
        otherwise.
        """

        for key, value in self.attributeNamesDict.items():
            if value == attribName:
                return key

        return attribName

    def textAttrsToDictionary(self, tokenString):
        """Converts a string of text attribute tokens of the form
        <key>:<value>; into a dictionary of keys and values.
        Text before the colon is the key and text afterwards is the
        value. If there is a final semi-colon, then it's ignored.

        Arguments:
        - tokenString: the string of tokens containing <key>:<value>; pairs.

        Returns a list containing two items:
        A list of the keys in the order they were extracted from the
        text attribute string and a dictionary of key/value items.
        """

        keyList = []
        dictionary = {}
        allTokens = tokenString.split(";")
        for token in allTokens:
            item = token.split(":")
            if len(item) == 2:
                key = item[0].strip()
                attribute = item[1].strip()
                keyList.append(key)
                dictionary[key] = attribute

        return [keyList, dictionary]

    def outputCharAttributes(self, keys, attributes):
        """Speak each of the text attributes given dictionary.

        Arguments:
        - attributes: a dictionary of text attributes to speak.
        """

        for key in keys:
            localizedKey = text_attribute_names.getTextAttributeName(key)
            if key in attributes:
                line = ""
                attribute = attributes[key]
                localizedValue = \
                    text_attribute_names.getTextAttributeName(attribute)
                if attribute:
                    key = self.getAtkNameForAttribute(key)
                    # If it's the 'weight' attribute and greater than 400, just
                    # speak it as bold, otherwise speak the weight.
                    #
                    if key == "weight" \
                       and (attribute == "bold" or int(attribute) > 400):
                        # Translators: bold as in the font sense.
                        #
                        line = _("bold")
                    elif key in ["left-margin", "right-margin"]:
                        # We need to test if we are getting a margin value
                        # that includes unit information (OOo now provides
                        # this). If not, then we will assume it's pixels.
                        #
                        numericPoint = locale.localeconv()["decimal_point"]
                        lastChar = attribute[len(attribute) - 1]
                        if lastChar == numericPoint or \
                           lastChar in self.digits:
                            # Translators: these represent the number of pixels
                            # for the left or right margins in a document.  We
                            # are hesitant to interpret the values -- they are
                            # given to us in some unknown form by the
                            # application, so we leave things in plural form
                            # here.
                            #
                            line = ngettext("%s %s pixel",
                                            "%s %s pixels",
                                            int(attribute)) % \
                                                (localizedKey, localizedValue)
                    elif key in ["indent", "size"]:
                        # In Gecko, we seem to get these values as a number
                        # immediately followed by "px". But we'll hedge our
                        # bet.
                        #
                        value = attribute.split("px")
                        if len(value) > 1:
                            line = ngettext("%s %s pixel",
                                            "%s %s pixels",
                                            float(value[0])) % \
                                            (localizedKey, value[0])
                    elif key == "family-name":
                        # In Gecko, we get a huge list and we just want the
                        # first one.  See:
                        # http://www.w3.org/TR/CSS2/fonts.html#font-family-prop
                        #
                        localizedValue = \
                            attribute.split(",")[0].strip().strip('"')

                    line = line or (localizedKey + " " + localizedValue)
                    speech.speak(line)

    def readCharAttributes(self, inputEvent=None):
        """Reads the attributes associated with the current text character.
        Calls outCharAttributes to speak a list of attributes. By default,
        a certain set of attributes will be spoken. If this is not desired,
        then individual application scripts should override this method to
        only speak the subset required.
        """

        try:
            text = orca_state.locusOfFocus.queryText()
        except:
            pass
        else:
            caretOffset = text.caretOffset

            # Creates dictionaries of the default attributes, plus the set
            # of attributes specific to the character at the caret offset.
            # Combine these two dictionaries and then extract just the
            # entries we are interested in.
            #
            defAttributes = text.getDefaultAttributes()
            debug.println(debug.LEVEL_FINEST, \
                "readCharAttributes: default text attributes: %s" % \
                defAttributes)
            [defUser, defDict] = self.textAttrsToDictionary(defAttributes)
            allAttributes = defDict

            charAttributes = text.getAttributes(caretOffset)
            if charAttributes[0]:
                [charList, charDict] = \
                    self.textAttrsToDictionary(charAttributes[0])

                # It looks like some applications like Evolution and Star
                # Office don't implement getDefaultAttributes(). In that
                # case, the best we can do is use the specific text
                # attributes for this character returned by getAttributes().
                #
                if allAttributes:
                    for key in charDict.keys():
                        allAttributes[key] = charDict[key]
                else:
                    allAttributes = charDict

            # Get a dictionary of text attributes that the user cares about.
            #
            [userAttrList, userAttrDict] = \
                self.textAttrsToDictionary(settings.enabledSpokenTextAttributes)

            # Create a dictionary of just the items we are interested in.
            # Always include size and family-name. For the others, if the
            # value is the default, then ignore it.
            #
            attributes = {}
            for key in userAttrList:
                if key in allAttributes:
                    textAttr = allAttributes.get(key)
                    userAttr = userAttrDict.get(key)
                    if textAttr != userAttr or len(userAttr) == 0:
                        attributes[key] = textAttr

            self.outputCharAttributes(userAttrList, attributes)

            # If this is a hypertext link, then let the user know:
            #
            if self.getLinkIndex(orca_state.locusOfFocus, caretOffset) >= 0:
                # Translators: this indicates that this piece of
                # text is a hypertext link.
                #
                speech.speak(_("link"))

        return True

    def reportScriptInfo(self, inputEvent=None):
        """Output useful information on the current script via speech
        and braille.  This information will be helpful to script writers.
        """

        infoString = "SCRIPT INFO: Script name='%s'" % self.name
        app = orca_state.locusOfFocus.getApplication()
        if orca_state.locusOfFocus and app:
            infoString += " Application name='%s'" \
                          % app.name

            try:
                infoString += " Toolkit name='%s'" \
                              % app.toolkitName
            except:
                infoString += " Toolkit unknown"

            try:
                infoString += " Version='%s'" \
                              % app.version
            except:
                infoString += " Version unknown"

            debug.println(debug.LEVEL_INFO, infoString)
            speech.speak(infoString)
            braille.displayMessage(infoString)

        return True

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

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

        # Translators: Orca normally intercepts all keyboard
        # commands and only passes them along to the current
        # application when they are not Orca commands.  This
        # command causes the next command issued to be passed
        # along to the current application, bypassing Orca's
        # interception of it.
        #
        speech.speak(_("Bypass mode enabled."))
        orca_state.bypassNextCommand = True
        return True

    def enterLearnMode(self, inputEvent=None):
        """Turns learn mode on.  The user must press the escape key to exit
        learn mode.

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

        if settings.learnModeEnabled:
            return True

        speech.speak(
            # Translators: Orca has a "Learn Mode" that will allow
            # the user to type any key on the keyboard and hear what
            # the effects of that key would be.  The effects might
            # be what Orca would do if it had a handler for the
            # particular key combination, or they might just be to
            # echo the name of the key if Orca doesn't have a handler.
            # This text here is what is spoken to the user.
            #
            _("Entering learn mode.  Press any key to hear its function.  " \
              "To exit learn mode, press the escape key."))

        # Translators: Orca has a "Learn Mode" that will allow
        # the user to type any key on the keyboard and hear what
        # the effects of that key would be.  The effects might
        # be what Orca would do if it had a handler for the
        # particular key combination, or they might just be to
        # echo the name of the key if Orca doesn't have a handler.
        # This text here is what is to be presented on the braille
        # display.
        #
        braille.displayMessage(_("Learn mode.  Press escape to exit."))
        settings.learnModeEnabled = True
        return True

    def pursueForFlatReview(self, obj):
        """Determines if we should look any further at the object
        for flat review."""

        try:
            state = obj.getState()
        except:
            debug.printException(debug.LEVEL_WARNING)
            return False
        else:
            return state.contains(pyatspi.STATE_SHOWING)

    def getShowingDescendants(self, parent):
        """Given a parent that manages its descendants, return a list of
        Accessible children that are actually showing.  This algorithm
        was inspired a little by the srw_elements_from_accessible logic
        in Gnopernicus, and makes the assumption that the children of
        an object that manages its descendants are arranged in a row
        and column format.

        Arguments:
        - parent: The accessible which manages its descendants

        Returns a list of Accessible descendants which are showing.
        """

        if not parent:
            return []

        if not parent.getState().contains(pyatspi.STATE_MANAGES_DESCENDANTS) \
           or parent.childCount <= 50:
            return []

        try:
            icomponent = parent.queryComponent()
        except NotImplementedError:
            return []

        descendants = []

        parentExtents = icomponent.getExtents(0)

        # [[[TODO: WDW - HACK related to GAIL bug where table column
        # headers seem to be ignored:
        # http://bugzilla.gnome.org/show_bug.cgi?id=325809.  The
        # problem is that this causes getAccessibleAtPoint to return
        # the cell effectively below the real cell at a given point,
        # making a mess of everything.  So...we just manually add in
        # showing headers for now.  The remainder of the logic below
        # accidentally accounts for this offset, yet it should also
        # work when bug 325809 is fixed.]]]
        #
        try:
            table = parent.queryTable()
        except NotImplementedError:
            table = None
            
        if table:
            for i in range(0, table.nColumns):
                header = table.getColumnHeader(i)
                if header:
                    extents = header.queryComponent().getExtents(0)
                    stateset = header.getState()
                    if stateset.contains(pyatspi.STATE_SHOWING) \
                       and (extents.x >= 0) and (extents.y >= 0) \
                       and (extents.width > 0) and (extents.height > 0) \
                       and self.visible(extents.x, extents.y,
                                        extents.width, extents.height,
                                        parentExtents.x, parentExtents.y,
                                        parentExtents.width,
                                        parentExtents.height):
                        descendants.append(header)

        # This algorithm goes left to right, top to bottom while attempting
        # to do *some* optimization over queries.  It could definitely be
        # improved. The gridSize is a minimal chunk to jump around in the
        # table.
        #
        gridSize = 7
        currentY = parentExtents.y
        while currentY < (parentExtents.y + parentExtents.height):
            currentX = parentExtents.x
            minHeight = sys.maxint
            while currentX < (parentExtents.x + parentExtents.width):
                child = \
                    icomponent.getAccessibleAtPoint(currentX, currentY + 1, 0)
                if child:
                    extents = child.queryComponent().getExtents(0)
                    if extents.x >= 0 and extents.y >= 0:
                        newX = extents.x + extents.width
                        minHeight = min(minHeight, extents.height)
                        if not descendants.count(child):
                            descendants.append(child)
                    else:
                        newX = currentX + gridSize
                else:
                    newX = currentX + gridSize
                if newX <= currentX:
                    currentX += gridSize
                else:
                    currentX = newX
            if minHeight == sys.maxint:
                minHeight = gridSize
            currentY += minHeight

        return descendants

    def visible(self,
                ax, ay, awidth, aheight,
                bx, by, bwidth, bheight):
        """Returns true if any portion of region 'a' is in region 'b'
        """
        highestBottom = min(ay + aheight, by + bheight)
        lowestTop = max(ay, by)

        leftMostRightEdge = min(ax + awidth, bx + bwidth)
        rightMostLeftEdge = max(ax, bx)

        visible = False

        if (lowestTop <= highestBottom) \
           and (rightMostLeftEdge <= leftMostRightEdge):
            visible = True
        elif (aheight == 0):
            if (awidth == 0):
                visible = (lowestTop == highestBottom) \
                          and (leftMostRightEdge == rightMostLeftEdge)
            else:
                visible = leftMostRightEdge <= rightMostLeftEdge
        elif (awidth == 0):
            visible = (lowestTop <= highestBottom)

        return visible

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

        if not self.flatReviewContext:
            self.flatReviewContext = self.flatReviewContextClass(self)
            self.justEnteredFlatReviewMode = True

            # Remember where the cursor currently was
            # when the user was in focus tracking mode.  We'll try to
            # keep the position the same as we move to characters above
            # and below us.
            #
            self.targetCursorCell = braille.cursorCell

        return self.flatReviewContext

    def toggleFlatReviewMode(self, inputEvent=None):
        """Toggles between flat review mode and focus tracking mode."""

        if self.flatReviewContext:
            self.drawOutline(-1, 0, 0, 0)
            self.flatReviewContext = None
            self.updateBraille(orca_state.locusOfFocus)
        else:
            context = self.getFlatReviewContext()
            [wordString, x, y, width, height] = \
                     context.getCurrent(flat_review.Context.WORD)
            self.drawOutline(x, y, width, height)
            self._reviewCurrentItem(inputEvent, self.targetCursorCell)

        return True

    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 postion the review cursor
        at that cell.  Otherwise, we will pan in display-sized increments
        to show the review cursor."""

        context = self.getFlatReviewContext()

        [regions, regionWithFocus] = context.getCurrentBrailleRegions()
        if not regions:
            regions = []
            regionWithFocus = None

        line = braille.Line()
        line.addRegions(regions)
        braille.setLines([line])
        braille.setFocus(regionWithFocus, False)
        if regionWithFocus:
            braille.panToOffset(regionWithFocus.brailleOffset \
                                + regionWithFocus.cursorOffset)

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

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

        context = self.getFlatReviewContext()
        [regions, regionWithFocus] = context.getCurrentBrailleRegions()
        for region in regions:
            if ((region.brailleOffset + len(region.string.decode("UTF-8"))) \
                   > braille.viewport[0]) \
                and (isinstance(region, braille.ReviewText) \
                     or isinstance(region, braille.ReviewComponent)):
                position = max(region.brailleOffset, braille.viewport[0])
                offset = position - region.brailleOffset
                self.targetCursorCell = region.brailleOffset \
                                        - braille.viewport[0]
                [word, charOffset] = region.zone.getWordAtOffset(offset)
                if word:
                    self.flatReviewContext.setCurrent(
                        word.zone.line.index,
                        word.zone.index,
                        word.index,
                        charOffset)
                else:
                    self.flatReviewContext.setCurrent(
                        region.zone.line.index,
                        region.zone.index,
                        0, # word index
                        0) # character index
                break

    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 self.flatReviewContext:
            if braille.beginningIsShowing:
                self.flatReviewContext.goBegin(flat_review.Context.LINE)
                self.reviewPreviousCharacter(inputEvent)
            else:
                braille.panLeft(panAmount)

            # This will update our target cursor cell
            #
            self._setFlatReviewContextToBeginningOfBrailleDisplay()

            [charString, x, y, width, height] = \
                self.flatReviewContext.getCurrent(flat_review.Context.CHAR)
            self.drawOutline(x, y, width, height)

            self.targetCursorCell = 1
            self.updateBrailleReview(self.targetCursorCell)
        elif braille.beginningIsShowing and orca_state.locusOfFocus \
             and self.isTextArea(orca_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 = orca_state.locusOfFocus.queryText()
            [lineString, startOffset, endOffset] = text.getTextAtOffset(
                text.caretOffset,
                pyatspi.TEXT_BOUNDARY_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 (orca_state.locusOfFocus.getRole() \
                    == pyatspi.ROLE_TERMINAL):
                context = self.getFlatReviewContext()
                context.goBegin(flat_review.Context.LINE)
                self.reviewPreviousCharacter(inputEvent)
        else:
            braille.panLeft(panAmount)
            braille.refresh(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 begininng 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 self.flatReviewContext:
            if braille.endIsShowing:
                self.flatReviewContext.goEnd(flat_review.Context.LINE)
                self.reviewNextCharacter(inputEvent)
            else:
                braille.panRight(panAmount)

            # This will update our target cursor cell
            #
            self._setFlatReviewContextToBeginningOfBrailleDisplay()

            [charString, x, y, width, height] = \
                self.flatReviewContext.getCurrent(flat_review.Context.CHAR)

            self.drawOutline(x, y, width, height)

            self.targetCursorCell = 1
            self.updateBrailleReview(self.targetCursorCell)
        elif braille.endIsShowing and orca_state.locusOfFocus \
             and self.isTextArea(orca_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 = orca_state.locusOfFocus.queryText()
            [lineString, startOffset, endOffset] = text.getTextAtOffset(
                text.caretOffset,
                pyatspi.TEXT_BOUNDARY_LINE_START)
            if endOffset < text.characterCount:
                text.setCaretOffset(endOffset)
        else:
            braille.panRight(panAmount)
            braille.refresh(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.flatReviewContext:
            return self.toggleFlatReviewMode(inputEvent)
        else:
            return braille.returnToRegionWithFocus(inputEvent)

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

        self.getFlatReviewContext().clickCurrent(1)
        return True

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

        self.getFlatReviewContext().clickCurrent(3)
        return True

    def reviewCurrentLine(self, inputEvent):
        """Brailles and speaks the current flat review line."""

        self._reviewCurrentLine(inputEvent, 1)
        self.lastReviewCurrentEvent = inputEvent

        return True

    def reviewSpellCurrentLine(self, inputEvent):
        """Brailles and spells the current flat review line."""

        self._reviewCurrentLine(inputEvent, 2)
        self.lastReviewCurrentEvent = inputEvent

        return True

    def reviewPhoneticCurrentLine(self, inputEvent):
        """Brailles and phonetically spells the current flat review line."""

        self._reviewCurrentLine(inputEvent, 3)
        self.lastReviewCurrentEvent = inputEvent

        return True

    def _reviewCurrentLine(self, inputEvent, speechType=1):
        """Presents the current flat review line via braille and speech.

        Arguments:
        - inputEvent - the current input event.
        - speechType - the desired presentation: speak (1), spell (2), or
                       phonetic (3)
        """

        context = self.getFlatReviewContext()

        [lineString, x, y, width, height] = \
                 context.getCurrent(flat_review.Context.LINE)
        self.drawOutline(x, y, width, height)

        # Don't announce anything from speech if the user used
        # the Braille display as an input device.
        #
        if not isinstance(inputEvent, input_event.BrailleEvent):
            if (not lineString) \
               or (not len(lineString)) \
               or (lineString == "\n"):
                # Translators: "blank" is a short word to mean the
                # user has navigated to an empty line.
                #
                speech.speak(_("blank"))
            elif lineString.isspace():
                # Translators: "white space" is a short phrase to mean the
                # user has navigated to a line with only whitespace on it.
                #
                speech.speak(_("white space"))
            elif lineString.isupper() and (speechType < 2 or speechType > 3):
                speech.speak(lineString, self.voices[settings.UPPERCASE_VOICE])
            elif speechType == 2:
                self.spellCurrentItem(lineString)
            elif speechType == 3:
                self.phoneticSpellCurrentItem(lineString)
            else:
                lineString = self.adjustForRepeats(lineString)
                speech.speak(lineString)

        self.updateBrailleReview()

        return True

    def reviewPreviousLine(self, inputEvent):
        """Moves the flat review context to the beginning of the
        previous line."""

        context = self.getFlatReviewContext()

        moved = context.goPrevious(flat_review.Context.LINE,
                                   flat_review.Context.WRAP_LINE)

        if moved:
            self._reviewCurrentLine(inputEvent)
            self.targetCursorCell = braille.cursorCell

        return True

    def reviewHome(self, inputEvent):
        """Moves the flat review context to the top left of the current
        window."""

        context = self.getFlatReviewContext()

        context.goBegin()

        self._reviewCurrentLine(inputEvent)
        self.targetCursorCell = braille.cursorCell

        return True

    def reviewNextLine(self, inputEvent):
        """Moves the flat review context to the beginning of the
        next line.  Places the flat review cursor at the beginning
        of the line."""

        context = self.getFlatReviewContext()

        moved = context.goNext(flat_review.Context.LINE,
                               flat_review.Context.WRAP_LINE)

        if moved:
            self._reviewCurrentLine(inputEvent)
            self.targetCursorCell = braille.cursorCell

        return True

    def reviewBottomLeft(self, inputEvent):
        """Moves the flat review context to the beginning of the
        last line in the window.  Places the flat review cursor at
        the beginning of the line."""

        context = self.getFlatReviewContext()

        context.goEnd(flat_review.Context.WINDOW)
        context.goBegin(flat_review.Context.LINE)
        self._reviewCurrentLine(inputEvent)
        self.targetCursorCell = braille.cursorCell

        return True

    def reviewEnd(self, inputEvent):
        """Moves the flat review context to the end of the
        last line in the window.  Places the flat review cursor
        at the end of the line."""

        context = self.getFlatReviewContext()
        context.goEnd()

        self._reviewCurrentLine(inputEvent)
        self.targetCursorCell = braille.cursorCell

        return True

    def reviewCurrentItem(self, inputEvent, targetCursorCell=0):
        """Brailles and speaks the current item to the user."""

        self._reviewCurrentItem(inputEvent, targetCursorCell, 1)
        self.lastReviewCurrentEvent = inputEvent

        return True

    def reviewSpellCurrentItem(self, inputEvent, targetCursorCell=0):
        """Brailles and spells the current item to the user."""

        self._reviewCurrentItem(inputEvent, targetCursorCell, 2)
        self.lastReviewCurrentEvent = inputEvent

        return True

    def reviewPhoneticCurrentItem(self, inputEvent, targetCursorCell=0):
        """Brailles and phonetically spells the current item to the user."""

        self._reviewCurrentItem(inputEvent, targetCursorCell, 3)
        self.lastReviewCurrentEvent = inputEvent

        return True

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

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

        for (charIndex, character) in enumerate(itemString.decode("UTF-8")):
            if character.isupper():
                speech.speak(character.encode("UTF-8"),
                             self.voices[settings.UPPERCASE_VOICE])
            else:
                speech.speak(character.encode("UTF-8"))

    def _reviewCurrentItem(self, inputEvent, targetCursorCell=0,
                           speechType=1):
        """Presents the current item to the user.

        Arguments:
        - inputEvent - the current input event.
        - targetCursorCell - if non-zero, the target braille cursor cell.
        - speechType - the desired presentation: speak (1), spell (2), or
                       phonetic (3).
        """

        context = self.getFlatReviewContext()
        [wordString, x, y, width, height] = \
                 context.getCurrent(flat_review.Context.WORD)
        self.drawOutline(x, y, width, height)

        # Don't announce anything from speech if the user used
        # the Braille display as an input device.
        #
        if not isinstance(inputEvent, input_event.BrailleEvent):
            if (not wordString) \
               or (not len(wordString)) \
               or (wordString == "\n"):
                # Translators: "blank" is a short word to mean the
                # user has navigated to an empty line.
                #
                speech.speak(_("blank"))
            else:
                [lineString, x, y, width, height] = \
                         context.getCurrent(flat_review.Context.LINE)
                if lineString == "\n":
                    # Translators: "blank" is a short word to mean the
                    # user has navigated to an empty line.
                    #
                    speech.speak(_("blank"))
                elif wordString.isspace():
                    # Translators: "white space" is a short phrase to mean the
                    # user has navigated to a line with only whitespace on it.
                    #
                    speech.speak(_("white space"))
                elif wordString.isupper() and speechType == 1:
                    speech.speak(wordString,
                                 self.voices[settings.UPPERCASE_VOICE])
                elif speechType == 2:
                    self.spellCurrentItem(wordString)
                elif speechType == 3:
                    self.phoneticSpellCurrentItem(wordString)
                elif speechType == 1:
                    wordString = self.adjustForRepeats(wordString)
                    speech.speak(wordString)

        self.updateBrailleReview(targetCursorCell)

        return True

    def reviewCurrentAccessible(self, inputEvent):
        context = self.getFlatReviewContext()
        [zoneString, x, y, width, height] = \
                 context.getCurrent(flat_review.Context.ZONE)
        self.drawOutline(x, y, width, height)

        # Don't announce anything from speech if the user used
        # the Braille display as an input device.
        #
        if not isinstance(inputEvent, input_event.BrailleEvent):
            utterances = self.speechGenerator.getSpeech(
                    context.getCurrentAccessible(), False)
            utterances.extend(self.tutorialGenerator.getTutorial(
                    context.getCurrentAccessible(), False))
            speech.speakUtterances(utterances)
        return True

    def reviewPreviousItem(self, inputEvent):
        """Moves the flat review context to the previous item.  Places
        the flat review cursor at the beginning of the item."""

        context = self.getFlatReviewContext()

        moved = context.goPrevious(flat_review.Context.WORD,
                                   flat_review.Context.WRAP_LINE)

        if moved:
            self._reviewCurrentItem(inputEvent)
            self.targetCursorCell = braille.cursorCell

        return True

    def reviewNextItem(self, inputEvent):
        """Moves the flat review context to the next item.  Places
        the flat review cursor at the beginning of the item."""

        context = self.getFlatReviewContext()

        moved = context.goNext(flat_review.Context.WORD,
                               flat_review.Context.WRAP_LINE)

        if moved:
            self._reviewCurrentItem(inputEvent)
            self.targetCursorCell = braille.cursorCell

        return True

    def reviewCurrentCharacter(self, inputEvent):
        """Brailles and speaks the current flat review character."""

        self._reviewCurrentCharacter(inputEvent, 1)
        self.lastReviewCurrentEvent = inputEvent

        return True

    def reviewSpellCurrentCharacter(self, inputEvent):
        """Brailles and 'spells' (phonetically) the current flat review
        character.
        """

        self._reviewCurrentCharacter(inputEvent, 2)
        self.lastReviewCurrentEvent = inputEvent

        return True

    def _reviewCurrentCharacter(self, inputEvent, speechType=1):
        """Presents the current flat review character via braille and speech.

        Arguments:
        - inputEvent - the current input event.
        - speechType - the desired presentation: speak (1) or phonetic (2)
        """

        context = self.getFlatReviewContext()

        [charString, x, y, width, height] = \
                 context.getCurrent(flat_review.Context.CHAR)
        self.drawOutline(x, y, width, height)

        # Don't announce anything from speech if the user used
        # the Braille display as an input device.
        #
        if not isinstance(inputEvent, input_event.BrailleEvent):
            if (not charString) or (not len(charString)):
                # Translators: "blank" is a short word to mean the
                # user has navigated to an empty line.
                #
                speech.speak(_("blank"))
            else:
                [lineString, x, y, width, height] = \
                         context.getCurrent(flat_review.Context.LINE)
                if lineString == "\n":
                    # Translators: "blank" is a short word to mean the
                    # user has navigated to an empty line.
                    #
                    speech.speak(_("blank"))
                elif speechType == 2:
                    self.phoneticSpellCurrentItem(charString)
                elif charString.decode("UTF-8").isupper():
                    speech.speakCharacter(charString,
                                          self.voices[settings.UPPERCASE_VOICE])
                else:
                    speech.speakCharacter(charString)

        self.updateBrailleReview()

        return True

    def reviewPreviousCharacter(self, inputEvent):
        """Moves the flat review context to the previous character.  Places
        the flat review cursor at character."""

        context = self.getFlatReviewContext()

        moved = context.goPrevious(flat_review.Context.CHAR,
                                   flat_review.Context.WRAP_LINE)

        if moved:
            self._reviewCurrentCharacter(inputEvent)
            self.targetCursorCell = braille.cursorCell

        return True

    def reviewEndOfLine(self, inputEvent):
        """Moves the flat review context to the end of the line.  Places
        the flat review cursor at the end of the line."""

        context = self.getFlatReviewContext()
        context.goEnd(flat_review.Context.LINE)

        self.reviewCurrentCharacter(inputEvent)
        self.targetCursorCell = braille.cursorCell

        return True

    def reviewNextCharacter(self, inputEvent):
        """Moves the flat review context to the next character.  Places
        the flat review cursor at character."""

        context = self.getFlatReviewContext()

        moved = context.goNext(flat_review.Context.CHAR,
                               flat_review.Context.WRAP_LINE)

        if moved:
            self._reviewCurrentCharacter(inputEvent)
            self.targetCursorCell = braille.cursorCell

        return True

    def reviewAbove(self, inputEvent):
        """Moves the flat review context to the character most directly
        above the current flat review cursor.  Places the flat review
        cursor at character."""

        context = self.getFlatReviewContext()

        moved = context.goAbove(flat_review.Context.CHAR,
                                flat_review.Context.WRAP_LINE)

        if moved:
            self._reviewCurrentItem(inputEvent, self.targetCursorCell)

        return True

    def reviewBelow(self, inputEvent):
        """Moves the flat review context to the character most directly
        below the current flat review cursor.  Places the flat review
        cursor at character."""

        context = self.getFlatReviewContext()

        moved = context.goBelow(flat_review.Context.CHAR,
                                flat_review.Context.WRAP_LINE)

        if moved:
            self._reviewCurrentItem(inputEvent, self.targetCursorCell)

        return True

    def showZones(self, inputEvent):
        """Debug routine to paint rectangles around the discrete
        interesting (e.g., text)  zones in the active window for
        this application.
        """

        flatReviewContext = self.getFlatReviewContext()
        lines = flatReviewContext.lines
        for line in lines:
            lineString = ""
            for zone in line.zones:
                lineString += " '%s' [%s]" % \
                          (zone.string, zone.accessible.getRoleName())
            debug.println(debug.LEVEL_OFF, lineString)
        self.flatReviewContext = None

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

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

        if not query:
            query = find.getLastQuery()
        if query:
            context = self.getFlatReviewContext()
            location = query.findQuery(context, self.justEnteredFlatReviewMode)
            if not location:
                # Translators: the Orca "Find" dialog allows a user to
                # search for text in a window and then move focus to
                # that text.  For example, they may want to find the
                # "OK" button.  This message lets them know a string
                # they were searching for was not found.
                #
                message = _("string not found")
                braille.displayMessage(message)
                speech.speak(message)
            else:
                context.setCurrent(location.lineIndex, location.zoneIndex, \
                                   location.wordIndex, location.charIndex)
                self.reviewCurrentItem(None)
                self.targetCursorCell = braille.cursorCell

    def findNext(self, inputEvent):
        """Searches forward for the next instance of the string
        searched for via the Orca 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:
            orca.showFindGUI()

    def findPrevious(self, inputEvent):
        """Searches backwards for the next instance of the string
        searched for via the Orca 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:
            orca.showFindGUI()

    def goToBookmark(self, inputEvent):
        """ Go to the bookmark indexed by inputEvent.hw_code.  Delegates to
        Bookmark.goToBookmark """
        bookmarks = self.getBookmarks()
        bookmarks.goToBookmark(inputEvent)

    def addBookmark(self, inputEvent):
        """ Add an in-page accessible object bookmark for this key.
        Delegates to Bookmark.addBookmark """
        bookmarks = self.getBookmarks()
        bookmarks.addBookmark(inputEvent)

    def bookmarkCurrentWhereAmI(self, inputEvent):
        """ Report "Where am I" information for this bookmark relative to the
        current pointer location.  Delegates to
        Bookmark.bookmarkCurrentWhereAmI"""
        bookmarks = self.getBookmarks()
        bookmarks.bookmarkCurrentWhereAmI(inputEvent)

    def saveBookmarks(self, inputEvent):
        """ Save the bookmarks for this script. Delegates to
        Bookmark.saveBookmarks """
        bookmarks = self.getBookmarks()
        bookmarks.saveBookmarks(inputEvent)

    def goToNextBookmark(self, inputEvent):
        """ Go to the next bookmark location.  If no bookmark has yet to be
        selected, the first bookmark will be used.  Delegates to
        Bookmark.goToNextBookmark """
        bookmarks = self.getBookmarks()
        bookmarks.goToNextBookmark(inputEvent)

    def goToPrevBookmark(self, inputEvent):
        """ Go to the previous bookmark location.  If no bookmark has yet to
        be selected, the first bookmark will be used.  Delegates to
        Bookmark.goToPrevBookmark """
        bookmarks = self.getBookmarks()
        bookmarks.goToPrevBookmark(inputEvent)

########################################################################
#                                                                      #
# DEBUG support.                                                       #
#                                                                      #
########################################################################

    def _isInterestingObj(self, obj):
        import inspect

        interesting = False

        if getattr(obj, "__class__", None):
            name = obj.__class__.__name__
            if name not in ["function",
                            "type",
                            "list",
                            "dict",
                            "tuple",
                            "wrapper_descriptor",
                            "module",
                            "method_descriptor",
                            "member_descriptor",
                            "instancemethod",
                            "builtin_function_or_method",
                            "frame",
                            "classmethod",
                            "classmethod_descriptor",
                            "_Environ",
                            "MemoryError",
                            "_Printer",
                            "_Helper",
                            "getset_descriptor",
                            "weakref",
                            "property",
                            "cell",
                            "staticmethod",
                            "EventListener",
                            "KeystrokeListener",
                            "KeyBinding",
                            "InputEventHandler",
                            "Rolename"]:
                try:
                    filename = inspect.getabsfile(obj.__class__)
                    if filename.index("orca"):
                        interesting = True
                except:
                    pass

        return interesting

    def _detectCycle(self, obj, visitedObjs, indent=""):
        """Attempts to discover a cycle in object references."""

        # [[[TODO: WDW - not sure this really works.]]]

        import gc
        visitedObjs.append(obj)
        for referent in gc.get_referents(obj):
            try:
                if visitedObjs.index(referent):
                    if self._isInterestingObj(referent):
                        print indent, "CYCLE!!!!", `referent`
                    break
            except:
                pass
            self._detectCycle(referent, visitedObjs, " " + indent)
        visitedObjs.remove(obj)

    def _printObjInfo(self, indent, obj):
        """Prints information about an object, if we care about it."""
        if self._isInterestingObj(obj):
            print indent, obj.__class__.__name__, `obj`

    def printMemoryUsageHandler(self, inputEvent):
        """Prints memory usage information."""
        print 'TODO: print something useful for memory debugging'

    def printAppsHandler(self, inputEvent=None):
        """Prints a list of all applications to stdout."""
        self.printApps()
        return True

    def printActiveAppHandler(self, inputEvent=None):
        """Prints the currently active application."""
        self.printActiveApp()
        return True

    def printAncestryHandler(self, inputEvent=None):
        """Prints the ancestry for the current locusOfFocus"""
        self.printAncestry(orca_state.locusOfFocus)
        return True

    def printHierarchyHandler(self, inputEvent=None):
        """Prints the application for the current locusOfFocus"""
        if orca_state.locusOfFocus:
            self.printHierarchy(orca_state.locusOfFocus.getApplication(),
                                orca_state.locusOfFocus)
        return True

# Routines that were previously in util.py, but that have now been moved
# here so that they can be customized in application scripts if so desired.
#

    def isSameObject(self, obj1, obj2):
        if (obj1 == obj2):
            return True
        elif (not obj1) or (not obj2):
            return False

        try:
            if (obj1.name != obj2.name) or (obj1.getRole() != obj2.getRole()):
                return False
            else:
                # Gecko sometimes creates multiple accessibles to represent
                # the same object.  If the two objects have the same name
                # and the same role, check the extents.  If those also match
                # then the two objects are for all intents and purposes the
                # same object.
                #
                extents1 = \
                    obj1.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
                extents2 = \
                    obj2.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
                if (extents1.x == extents2.x) and \
                   (extents1.y == extents2.y) and \
                   (extents1.width == extents2.width) and \
                   (extents1.height == extents2.height):
                    return True

            # When we're looking at children of objects that manage
            # their descendants, we will often get different objects
            # that point to the same logical child.  We want to be able
            # to determine if two objects are in fact pointing to the
            # same child.
            # If we cannot do so easily (i.e., object equivalence), we examine
            # the hierarchy and the object index at each level.
            #
            parent1 = obj1
            parent2 = obj2
            while (parent1 and parent2 and \
                    parent1.getState().contains( \
                        pyatspi.STATE_TRANSIENT) and \
                    parent2.getState().contains(pyatspi.STATE_TRANSIENT)):
                if parent1.getIndexInParent() != parent2.getIndexInParent():
                    return False
                parent1 = parent1.parent
                parent2 = parent2.parent
            if parent1 and parent2 and parent1 == parent2:
                return self.getRealActiveDescendant(obj1).name == \
                       self.getRealActiveDescendant(obj2).name
        except:
            pass

        # In java applications, TRANSIENT state is missing for tree items
        # (fix for bug #352250)
        #
        try:
            parent1 = obj1
            parent2 = obj2
            while parent1 and parent2 and \
                    parent1.getRole() == pyatspi.ROLE_LABEL and \
                    parent2.getRole() == pyatspi.ROLE_LABEL:
                if parent1.getIndexInParent() != parent2.getIndexInParent():
                    return False
                parent1 = parent1.parent
                parent2 = parent2.parent
            if parent1 and parent2 and parent1 == parent2:
                return True
        except:
            pass

        return False

    def appendString(self, text, newText, delimiter=" "):
        """Appends the newText to the given text with the delimiter in between
        and returns the new string.  Edge cases, such as no initial text or
        no newText, are handled gracefully."""

        if (not newText) or (len(newText) == 0):
            return text
        elif text and len(text):
            return text + delimiter + newText
        else:
            return newText

    def __hasLabelForRelation(self, label):
        """Check if label has a LABEL_FOR relation

        Arguments:
        - label: the label in question

        Returns TRUE if label has a LABEL_FOR relation.
        """
        if (not label) or (label.getRole() != pyatspi.ROLE_LABEL):
            return False

        relations = label.getRelationSet()

        for relation in relations:
            if relation.getRelationType() \
                   == pyatspi.RELATION_LABEL_FOR:
                return True

        return False

    def __isLabeling(self, label, obj):
        """Check if label is connected via  LABEL_FOR relation with object

        Arguments:
        - obj: the object in question
        - labeled: the label in question

        Returns TRUE if label has a relation LABEL_FOR for object.
        """

        if (not obj) \
           or (not label) \
           or (label.getRole() != pyatspi.ROLE_LABEL):
            return False

        relations = label.getRelationSet()
        if not relations:
            return False

        for relation in relations:
            if relation.getRelationType() \
                   == pyatspi.RELATION_LABEL_FOR:

                for i in range(0, relation.getNTargets()):
                    target = relation.getTarget(i)
                    if target == obj:
                        return True

        return False

    def getUnicodeCurrencySymbols(self):
        """Return a list of the unicode currency symbols, populating the list
        if this is the first time that this routine has been called.

        Returns a list of unicode currency symbols.
        """

        if not self._unicodeCurrencySymbols:
            self._unicodeCurrencySymbols = [ \
                u'\u0024',     # dollar sign
                u'\u00A2',     # cent sign
                u'\u00A3',     # pound sign
                u'\u00A4',     # currency sign
                u'\u00A5',     # yen sign
                u'\u0192',     # latin small letter f with hook
                u'\u060B',     # afghani sign
                u'\u09F2',     # bengali rupee mark
                u'\u09F3',     # bengali rupee sign
                u'\u0AF1',     # gujarati rupee sign
                u'\u0BF9',     # tamil rupee sign
                u'\u0E3F',     # thai currency symbol baht
                u'\u17DB',     # khmer currency symbol riel
                u'\u2133',     # script capital m
                u'\u5143',     # cjk unified ideograph-5143
                u'\u5186',     # cjk unified ideograph-5186
                u'\u5706',     # cjk unified ideograph-5706
                u'\u5713',     # cjk unified ideograph-5713
                u'\uFDFC',     # rial sign
            ]

            # Add 20A0 (EURO-CURRENCY SIGN) to 20B5 (CEDI SIGN)
            #
            for ordChar in range(ord(u'\u20A0'), ord(u'\u20B5') + 1):
                self._unicodeCurrencySymbols.append(unichr(ordChar))

        return self._unicodeCurrencySymbols

    def findDisplayedLabel(self, obj):
        """Return a list of the objects that are labelling this object.

        Argument:
        - obj: the object in question

        Returns a list of the objects that are labelling this object.
        """

        # For some reason, some objects are labelled by the same thing
        # more than once.  Go figure, but we need to check for this.
        #
        label = []
        relations = obj.getRelationSet()
        allTargets = []

        for relation in relations:
            if relation.getRelationType() \
                   == pyatspi.RELATION_LABELLED_BY:

                # The object can be labelled by more than one thing, so we just
                # get all the labels (from unique objects) and append them
                # together.  An example of such objects live in the "Basic"
                # page of the gnome-accessibility-keyboard-properties app.
                # The "Delay" and "Speed" objects are labelled both by
                # their names and units.
                #
                for i in range(0, relation.getNTargets()):
                    target = relation.getTarget(i)
                    if not target in allTargets:
                        allTargets.append(target)
                        label.append(target)

        # [[[TODO: HACK - we've discovered oddness in hierarchies such as
        # the gedit Edit->Preferences dialog.  In this dialog, we have
        # labeled groupings of objects.  The grouping is done via a FILLER
        # with two children - one child is the overall label, and the
        # other is the container for the grouped objects.  When we detect
        # this, we add the label to the overall context.
        #
        # We are also looking for objects which have a PANEL or a FILLER as
        # parent, and its parent has more children. Through these children,
        # a potential label with LABEL_FOR relation may exists. We want to
        # present this label.
        # This case can be seen in FileChooserDemo application, in Open dialog
        # window, the line with "Look In" label, a combobox and some
        # presentation buttons.
        #
        # Finally, we are searching the hierarchy of embedded components for
        # children that are labels.]]]
        #
        if not len(label):
            potentialLabels = []
            useLabel = False
            if (obj.getRole() == pyatspi.ROLE_EMBEDDED):
                candidate = obj
                while candidate.childCount:
                    candidate = candidate[0]
                # The parent of this object may contain labels
                # or it may contain filler that contains labels.
                #
                candidate = candidate.parent
                for child in candidate:
                    if child.getRole() == pyatspi.ROLE_FILLER:
                        candidate = child
                        break
                # If there are labels in this embedded component,
                # they should be here.
                #
                for child in candidate:
                    if child.getRole() == pyatspi.ROLE_LABEL:
                        useLabel = True
                        potentialLabels.append(child)
            elif ((obj.getRole() == pyatspi.ROLE_FILLER) \
                    or (obj.getRole() == pyatspi.ROLE_PANEL)) \
                and (obj.childCount == 2):
                child0, child1 = obj
                child0_role = child0.getRole()
                child1_role = child1.getRole()
                if child0_role == pyatspi.ROLE_LABEL \
                    and not self.__hasLabelForRelation(child0) \
                    and child1_role in [pyatspi.ROLE_FILLER, \
                                             pyatspi.ROLE_PANEL]:
                    useLabel = True
                    potentialLabels.append(child0)
                elif child1_role == pyatspi.ROLE_LABEL \
                    and not self.__hasLabelForRelation(child1) \
                    and child0_role in [pyatspi.ROLE_FILLER, \
                                             pyatspi.ROLE_PANEL]:
                    useLabel = True
                    potentialLabels.append(child1)
            else:
                parent = obj.parent
                if parent and \
                    ((parent.getRole() == pyatspi.ROLE_FILLER) \
                            or (parent.getRole() == pyatspi.ROLE_PANEL)):
                    for potentialLabel in parent:
                        try:
                            useLabel = self.__isLabeling(potentialLabel, obj)
                            if useLabel:
                                potentialLabels.append(potentialLabel)
                                break
                        except:
                            pass

            if useLabel and len(potentialLabels):
                label = potentialLabels

        return label

    def getDisplayedLabel(self, obj):
        """If there is an object labelling the given object, return the
        text being displayed for the object labelling this object.
        Otherwise, return None.

        Argument:
        - obj: the object in question

        Returns the string of the object labelling this object, or None
        if there is nothing of interest here.
        """

        labelString = None
        labels = self.findDisplayedLabel(obj)
        for label in labels:
            labelString = self.appendString(labelString,
                                            self.getDisplayedText(label))

        return labelString

    def __getDisplayedTextInComboBox(self, combo):

        """Returns the text being displayed in a combo box.  If nothing is
        displayed, then None is returned.

        Arguments:
        - combo: the combo box

        Returns the text in the combo box or an empty string if nothing is
        displayed.
        """

        displayedText = None

        # Find the text displayed in the combo box.  This is either:
        #
        # 1) The last text object that's a child of the combo box
        # 2) The selected child of the combo box.
        # 3) The contents of the text of the combo box itself when
        #    treated as a text object.
        #
        # Preference is given to #1, if it exists.
        #
        # If the label of the combo box is the same as the utterance for
        # the child object, then this utterance is only displayed once.
        #
        # [[[TODO: WDW - Combo boxes are complex beasts.  This algorithm
        # needs serious work.  Logged as bugzilla bug 319745.]]]
        #
        textObj = None
        for child in combo:
            if child and child.getRole() == pyatspi.ROLE_TEXT:
                textObj = child

        if textObj:
            [displayedText, caretOffset, startOffset] = \
                self.getTextLineAtCaret(textObj)
            #print "TEXTOBJ", displayedText
        else:
            try:
                comboSelection = combo.querySelection()
                selectedItem = comboSelection.getSelectedChild(0)
            except:
                selectedItem = None

            if selectedItem:
                displayedText = self.getDisplayedText(selectedItem)
                #print "SELECTEDITEM", displayedText
            elif combo.name and len(combo.name):
                # We give preference to the name over the text because
                # the text for combo boxes seems to never change in
                # some cases.  The main one where we see this is in
                # the gaim "Join Chat" window.
                #
                displayedText = combo.name
                #print "NAME", displayedText
            else:
                [displayedText, caretOffset, startOffset] = \
                    self.getTextLineAtCaret(combo)
                # Set to None instead of empty string.
                displayedText = displayedText or None
                #print "TEXT", displayedText

        return displayedText

    def getDisplayedText(self, obj):
        """Returns the text being displayed for an object.

        Arguments:
        - obj: the object

        Returns the text being displayed for an object or None if there isn't
        any text being shown.
        """

        displayedText = None

        role = obj.getRole()
        if role == pyatspi.ROLE_COMBO_BOX:
            return self.__getDisplayedTextInComboBox(obj)

        # The accessible text of an object is used to represent what is
        # drawn on the screen.
        #
        try:
            text = obj.queryText()
        except NotImplementedError:
            pass
        else:
            displayedText = text.getText(0, -1)

            # [[[WDW - HACK to account for things such as Gecko that want
            # to use the EMBEDDED_OBJECT_CHARACTER on a label to hold the
            # object that has the real accessible text for the label.  We
            # detect this by the specfic case where the text for the
            # current object is a single EMBEDDED_OBJECT_CHARACTER.  In
            # this case, we look to the child for the real text.]]]
            #
            unicodeText = displayedText.decode("UTF-8")
            if unicodeText \
               and (len(unicodeText) == 1) \
               and (unicodeText[0] == self.EMBEDDED_OBJECT_CHARACTER) \
               and obj.childCount > 0:
                try:
                    displayedText = self.getDisplayedText(obj[0])
                except:
                    debug.printException(debug.LEVEL_WARNING)
            elif unicodeText:
                # [[[TODO: HACK - Welll.....we'll just plain ignore any
                # text with EMBEDDED_OBJECT_CHARACTERs here.  We still need a
                # general case to handle this stuff and expand objects
                # with EMBEDDED_OBJECT_CHARACTERs.]]]
                #
                for i in range(0, len(unicodeText)):
                    if unicodeText[i] == self.EMBEDDED_OBJECT_CHARACTER:
                        displayedText = None
                        break

        if not displayedText:
            displayedText = obj.name

        # [[[WDW - HACK because push buttons can have labels as their
        # children.  An example of this is the Font: button on the General
        # tab in the Editing Profile dialog in gnome-terminal.
        #
        if not displayedText and role == pyatspi.ROLE_PUSH_BUTTON:
            for child in obj:
                if child.getRole() == pyatspi.ROLE_LABEL:
                    childText = self.getDisplayedText(child)
                    if childText and len(childText):
                        displayedText = self.appendString(displayedText,
                                                          childText)

        return displayedText

    def getTextForValue(self, obj):
        """Returns the text to be displayed for the object's current value.

        Arguments:
        - obj: the Accessible object that may or may not have a value.

        Returns a string representing the value.
        """

        # Use ARIA "valuetext" attribute if present.  See 
        # http://bugzilla.gnome.org/show_bug.cgi?id=552965
        #
        attributes = obj.getAttributes()
        for attribute in attributes:
            if attribute.startswith("valuetext"):
                return attribute[10:]

        try:
            value = obj.queryValue()
        except NotImplementedError:
            return ""

        # OK, this craziness is all about trying to figure out the most
        # meaningful formatting string for the floating point values.
        # The number of places to the right of the decimal point should
        # be set by the minimumIncrement, but the minimumIncrement isn't
        # always set.  So...we'll default the minimumIncrement to 1/100
        # of the range.  But, if max == min, then we'll just go for showing
        # them off to two meaningful digits.
        #
        try:
            minimumIncrement = value.minimumIncrement
        except:
            minimumIncrement = 0.0

        if minimumIncrement == 0.0:
            minimumIncrement = (value.maximumValue - value.minimumValue) \
                               / 100.0

        try:
            decimalPlaces = max(0, -math.log10(minimumIncrement))
        except:
            try:
                decimalPlaces = max(0, -math.log10(value.minimumValue))
            except:
                try:
                    decimalPlaces = max(0, -math.log10(value.maximumValue))
                except:
                    decimalPlaces = 0

        formatter = "%%.%df" % decimalPlaces
        valueString = formatter % value.currentValue
        #minString   = formatter % value.minimumValue
        #maxString   = formatter % value.maximumValue

        # [[[TODO: WDW - probably want to do this as a percentage at some
        # point?  Logged as bugzilla bug 319743.]]]
        #
        return valueString

    def findFocusedObject(self, root):
        """Returns the accessible that has focus under or including the
        given root.

        TODO: This will currently traverse all children, whether they are
        visible or not and/or whether they are children of parents that
        manage their descendants.  At some point, this method should be
        optimized to take such things into account.

        Arguments:
        - root: the root object where to start searching

        Returns the object with the FOCUSED state or None if no object with
        the FOCUSED state can be found.
        """

        if root.getState().contains(pyatspi.STATE_FOCUSED):
            return root

        for child in root:
            try:
                candidate = self.findFocusedObject(child)
                if candidate:
                    return candidate
            except:
                pass

        return None

    def getRealActiveDescendant(self, obj):
        """Given an object that should be a child of an object that
        manages its descendants, return the child that is the real
        active descendant carrying useful information.

        Arguments:
        - obj: an object that should be a child of an object that
        manages its descendants.
        """

        # If obj is a table cell and all of it's children are table cells
        # (probably cell renderers), then return the first child which has
        # a non zero length text string. If no such object is found, just
        # fall through and use the default approach below. See bug #376791
        # for more details.
        #
        if obj.getRole() == pyatspi.ROLE_TABLE_CELL and obj.childCount:
            nonTableCellFound = False
            for child in obj:
                if child.getRole() != pyatspi.ROLE_TABLE_CELL:
                    nonTableCellFound = True
            if not nonTableCellFound:
                for child in obj:
                    try:
                        text = child.queryText()
                    except NotImplementedError:
                        continue
                    else:
                        if text.getText(0, -1):
                            return child

        # [[[TODO: WDW - this is an odd hacky thing I've somewhat drawn
        # from Gnopernicus.  The notion here is that we get an active
        # descendant changed event, but that object tends to have children
        # itself and we need to decide what to do.  Well...the idea here
        # is that the last child (Gnopernicus chooses child(1)), tends to
        # be the child with information.  The previous children tend to
        # be non-text or just there for spacing or something.  You will
        # see this in the various table demos of gtk-demo and you will
        # also see this in the Contact Source Selector in Evolution.
        #
        # Just note that this is most likely not a really good solution
        # for the general case.  That needs more thought.  But, this
        # comment is here to remind us this is being done in poor taste
        # and we need to eventually clean up our act.]]]
        #
        if obj and obj.childCount:
            return obj[-1]
        else:
            return obj

    def isDesiredFocusedItem(self, obj, rolesList):
        """Called to determine if the given object and it's hierarchy of
           parent objects, each have the desired roles.

        Arguments:
        - obj: the accessible object to check.
        - rolesList: the list of desired roles for the components and the
          hierarchy of its parents.

        Returns True if all roles match.
        """
        current = obj
        for role in rolesList:
            if current is None:
                return False

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

            if isinstance(role[0], str):
                current_role = current.getRoleName()
            else:
                current_role = current.getRole()

            if not current_role in role:
                return False
            current = current.parent

        return True

    def speakMisspeltWord(self, allTokens, badWord):
        """Called by various spell checking routine to speak the misspelt word,
           plus the context that it is being used in.

        Arguments:
        - allTokens: a list of all the words.
        - badWord: the misspelt word.
        """

        # Create an utterance to speak consisting of the misspelt
        # word plus the context where it is used (upto five words
        # to either side of it).
        #
        for i in range(0, len(allTokens)):
            if allTokens[i].startswith(badWord):
                minIndex = i - 5
                if minIndex < 0:
                    minIndex = 0
                maxIndex = i + 5
                if maxIndex > (len(allTokens) - 1):
                    maxIndex = len(allTokens) - 1

                # Translators: Orca will provide more compelling output of
                # the spell checking dialog in some applications.  The first
                # thing it does is let them know what the misspelled word
                # is.
                #
                utterances = [_("Misspelled word: %s") % badWord]

                # Translators: Orca will provide more compelling output of
                # the spell checking dialog in some applications.  The second
                # thing it does is give the phrase containing the misspelled
                # word in the document.  This is known as the context.
                #
                contextPhrase = " ".join(allTokens[minIndex:maxIndex+1])
                utterances.append(_("Context is %s") % contextPhrase)

                # Turn the list of utterances into a string.
                text = " ".join(utterances)
                speech.speak(text)

    def textLines(self, obj):
        """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.
        """

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

        length = text.characterCount
        offset = text.caretOffset

        # Determine the correct "say all by" mode to use.
        #
        if settings.sayAllStyle == settings.SAYALL_STYLE_SENTENCE:
            mode = pyatspi.TEXT_BOUNDARY_SENTENCE_END
        elif settings.sayAllStyle == settings.SAYALL_STYLE_LINE:
            mode = pyatspi.TEXT_BOUNDARY_LINE_START
        else:
            mode = pyatspi.TEXT_BOUNDARY_LINE_START

        # Get the next line of text to read
        #
        done = False
        while not done:
            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 = pyatspi.TEXT_BOUNDARY_LINE_START
                    [lineString, startOffset, endOffset] = \
                        text.getTextAtOffset(offset, mode)

                # [[[WDW - HACK: well...gnome-terminal sometimes wants to
                # give us outrageous values back from getTextAtOffset
                # (see http://bugzilla.gnome.org/show_bug.cgi?id=343133),
                # so we try to handle it.]]]
                #
                if startOffset < 0:
                    break

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

                lineString = self.adjustForRepeats(lineString)
                if lineString.isupper():
                    voice = settings.voices[settings.UPPERCASE_VOICE]
                else:
                    voice = settings.voices[settings.DEFAULT_VOICE]

                yield [speechserver.SayAllContext(obj, lineString,
                                                  startOffset, endOffset),
                       voice]

            moreLines = False
            relations = obj.getRelationSet()
            for relation in relations:
                if relation.getRelationType()  \
                       == pyatspi.RELATION_FLOWS_TO:
                    obj = relation.getTarget(0)

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

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

    def _addRepeatSegment(self, segment, line, respectPunctuation=True):
        """Add in the latest line segment, adjusting for repeat characters
        and punctuation.

        Arguments:
        - segment: the segment of repeated characters.
        - line: the current built-up line to characters to speak.
        - respectPunctuation: if False, ignore punctuation level.

        Returns: the current built-up line plus the new segment, after
        adjusting for repeat character counts and punctuation.
        """

        style = settings.verbalizePunctuationStyle
        isPunctChar = True
        try:
            level, action = punctuation_settings.getPunctuationInfo(segment[0])
        except:
            isPunctChar = False
        count = len(segment)
        if (count >= settings.repeatCharacterLimit) \
           and (not segment[0] in self.whitespace):
            if (not respectPunctuation) \
               or (isPunctChar and (style <= level)):
                repeatChar = chnames.getCharacterName(segment[0])
                # Translators: Orca will tell you how many characters
                # are repeated on a line of text.  For example: "22
                # space characters".  The %d is the number and the %s
                # is the spoken word for the character.
                #
                line += " " \
                     + ngettext("%(count)d %(repeatChar)s character",
                                "%(count)d %(repeatChar)s characters",
                                count) \
                       % {"count" : count, "repeatChar": repeatChar}
            else:
                line += segment
        else:
            line += segment

        return line

    def adjustForLinks(self, obj, line, startOffset):
        """Adjust line to include the word "link" after any hypertext links.

        Arguments:
        - obj: the accessible object that this line came from.
        - line: the string to adjust for links.
        - startOffset: the caret offset at the start of the line.

        Returns: a new line adjusted to add the speaking of "link" after
        text which is also a link.
        """

        line = line.decode("UTF-8")
        endOffset = startOffset + len(line)

        try:
            hyperText = obj.queryHypertext()
            nLinks = hyperText.getNLinks()
        except:
            nLinks = 0

        adjustedLine = list(line)
        for n in range(nLinks, 0, -1):
            link = hyperText.getLink(n - 1)

            # We only care about links in the string, line:
            #
            if startOffset < link.endIndex < endOffset:
                index = link.endIndex - startOffset
            elif startOffset <= link.startIndex < endOffset:
                index = len(line) - 1
            else:
                continue

            # Translators: this indicates that this piece of
            # text is a hypertext link.
            #
            linkString = " " + _("link")

            # If the link was not followed by a whitespace or punctuation
            # character, then add in a space to make it more presentable.
            #
            nextChar = adjustedLine[index]
            if not (nextChar in self.whitespace \
                    or punctuation_settings.getPunctuationInfo(nextChar)):
                linkString += " "
            adjustedLine[index:index] = linkString

        return "".join(adjustedLine).encode("UTF-8")

    def adjustForRepeats(self, line):
        """Adjust line to include repeat character counts.
        As some people will want this and others might not,
        there is a setting in settings.py that determines
        whether this functionality is enabled.

        repeatCharacterLimit = <n>

        If <n> is 0, then there would be no repeat characters.
        Otherwise <n> would be the number of same characters (or more)
        in a row that cause the repeat character count output.
        If the value is set to 1, 2 or 3 then it's treated as if it was
        zero. In other words, no repeat character count is given.

        Arguments:
        - line: the string to adjust for repeat character counts.

        Returns: a new line adjusted for repeat character counts (if enabled).
        """

        line = line.decode("UTF-8")

        if (len(line) < 4) or (settings.repeatCharacterLimit < 4):
            return line.encode("UTF-8")

        newLine = u''
        segment = lastChar = line[0]

        multipleChars = False
        for i in range(1, len(line)):
            if line[i] == lastChar:
                segment += line[i]
            else:
                multipleChars = True
                newLine = self._addRepeatSegment(segment, newLine)
                segment = line[i]

            lastChar = line[i]

        newLine = self._addRepeatSegment(segment, newLine, multipleChars)

        # Pylint is confused and flags this with the following error:
        #
        # E1103:5188:Script.adjustForRepeats: Instance of 'True' has
        # no 'encode' member (but some types could not be inferred)
        #
        # We know newLine is a unicode string, so we'll just tell pylint
        # that we know what we are doing.
        #
        # pylint: disable-msg=E1103

        return newLine.encode("UTF-8")

    def _getPronunciationForSegment(self, segment):
        """Adjust the word segment to potentially replace it with what
        those words actually sound like. Two pronunciation dictionaries
        are checked. First the application specific one (which might be
        empty), then the default (global) one.

        Arguments:
        - segment: the string to adjust for words in the pronunciation
          dictionaries.

        Returns: a new word segment adjusted for words found in the
        pronunciation dictionaries, or the original word segment if there
        was no dictionary entry.
        """

        newSegment = pronunciation_dict.getPronunciation(segment,
                                     self.app_pronunciation_dict)
        if newSegment == segment:
            newSegment = pronunciation_dict.getPronunciation(segment)

        return newSegment

    def adjustForPronunciation(self, line):
        """Adjust the line to replace words in the pronunciation dictionary,
        with what those words actually sound like.

        Arguments:
        - line: the string to adjust for words in the pronunciation dictionary.

        Returns: a new line adjusted for words found in the pronunciation
        dictionary.
        """

        line = line.decode("UTF-8")
        newLine = segment = u''

        for i in range(0, len(line)):
            if self.isWordDelimiter(line[i]):
                if len(segment) != 0:
                    newLine = newLine + \
                              self._getPronunciationForSegment(segment)
                newLine = newLine + line[i]
                segment = u''
            else:
                segment += line[i]

        if len(segment) != 0:
            newLine = newLine + self._getPronunciationForSegment(segment)

        return newLine.encode("UTF-8")

    def getLinkIndex(self, obj, characterIndex):
        """A brute force method to see if an offset is a link.  This
        is provided because not all Accessible Hypertext implementations
        properly support the getLinkIndex method.  Returns an index of
        0 or greater of the characterIndex is on a hyperlink.

        Arguments:
        -obj: the Accessible object with the Accessible Hypertext specialization
        -characterIndex: the text position to check
        """

        if not obj:
            return -1

        try:
            obj.queryText()
        except NotImplementedError:
            return -1

        try:
            hypertext = obj.queryHypertext()
        except NotImplementedError:
            return -1

        for i in xrange(hypertext.getNLinks()):
            link = hypertext.getLink(i)
            if (characterIndex >= link.startIndex) \
               and (characterIndex <= link.endIndex):
                return i

        return -1

    def getCellIndex(self, obj):
        """Returns the index of the cell which should be used with the
        table interface.  This is necessary because in some apps we
        cannot count on getIndexInParent() returning the index we need.

        Arguments:
        -obj: the table cell whose index we need.
        """
        return obj.getIndexInParent()

    def isSentenceDelimiter(self, currentChar, previousChar):
        """Returns True if we are positioned at the end of a sentence.
        This is determined by checking if the current character is a 
        white space character and the previous character is one of the 
        normal end-of-sentence punctuation characters.

        Arguments:
        - currentChar:  the current character
        - previousChar: the previous character

        Returns True if the given character is a sentence delimiter.
        """

        if not isinstance(currentChar, unicode):
            currentChar = currentChar.decode("UTF-8")

        if not isinstance(previousChar, unicode):
            previousChar = previousChar.decode("UTF-8")

        if currentChar == '\r' or currentChar == '\n':
            return True

        return (currentChar in self.whitespace and previousChar in '!.?:;')

    def isWordDelimiter(self, character):
        """Returns True if the given character is a word delimiter.

        Arguments:
        - character: the character in question

        Returns True if the given character is a word delimiter.
        """

        if not isinstance(character, unicode):
            character = character.decode("UTF-8")

        return (character in self.whitespace) \
               or (character in '!*+,-./:;<=>?@[\]^_{|}') \
               or (character == self.NO_BREAK_SPACE_CHARACTER)

    def getFrame(self, obj):
        """Returns the frame containing this object, or None if this object
        is not inside a frame.

        Arguments:
        - obj: the Accessible object
        """

        debug.println(debug.LEVEL_FINEST,
                      "Finding frame for source.name="
                      + obj.name or "None")

        while obj \
              and (obj != obj.parent) \
              and (obj.getRole() != pyatspi.ROLE_FRAME):
            obj = obj.parent
            if obj:
                debug.println(debug.LEVEL_FINEST, "--> obj.name="
                          + obj.name or "None")

        if obj and (obj.getRole() == pyatspi.ROLE_FRAME):
            pass
        else:
            obj = None

        return obj

    def getTopLevel(self, obj):
        """Returns the top-level object (frame, dialog ...) containing this
        object, or None if this object is not inside a top-level object.

        Arguments:
        - obj: the Accessible object
        """

        debug.println(debug.LEVEL_FINEST,
                      "Finding top-level object for source.name="
                      + obj.name or "None")

        while obj \
              and obj.parent \
              and (obj != obj.parent) \
              and (obj.parent.getRole() != pyatspi.ROLE_APPLICATION):
            obj = obj.parent
            debug.println(debug.LEVEL_FINEST, "--> obj.name="
                          + obj.name or "None")

        if obj and obj.parent and \
           (obj.parent.getRole() == pyatspi.ROLE_APPLICATION):
            pass
        else:
            obj = None

        return obj

    def getTopLevelName(self, obj):
        """ Returns the name of the top-level object. See getTopLevel.
        """
        top = self.getTopLevel(obj)
        if (not top) or (not top.name):
            return ""
        else:
            return top.name

    def getTextLineAtCaret(self, obj, offset=None):
        """Gets the line of text where the caret is.

        Argument:
        - obj: an Accessible object that implements the AccessibleText
          interface
        - offset: an optional caret offset to use. (Not used here at the
          moment, but needed in the Gecko script)

        Returns the [string, caretOffset, startOffset] for the line of text
        where the caret is.
        """

        # Get the the AccessibleText interrface
        #
        try:
            text = obj.queryText()
        except NotImplementedError:
            return ["", 0, 0]

        # The caret 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 text.caretOffset == text.characterCount:
            caretOffset = max(0, text.caretOffset - 1)
            character = text.getText(caretOffset,
                                     caretOffset + 1).decode("UTF-8")
        else:
            caretOffset = text.caretOffset
            character = None

        if (text.caretOffset == text.characterCount) \
            and (character == "\n"):
            content = ""
            startOffset = caretOffset
        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 (text.characterCount == 1):
                lineString = text.getText(caretOffset, caretOffset + 1)
                startOffset = caretOffset
            else:
                [lineString, startOffset, endOffset] = text.getTextAtOffset(
                    caretOffset, pyatspi.TEXT_BOUNDARY_LINE_START)

            # Sometimes we get the trailing line-feed-- remove it
            #
            content = lineString.decode("UTF-8")
            if content[-1:] == "\n":
                content = content[:-1]

        return [content.encode("UTF-8"), text.caretOffset, startOffset]

    def getNodeLevel(self, obj):
        """Determines the node level of this object if it is in a tree
        relation, with 0 being the top level node.  If this object is
        not in a tree relation, then -1 will be returned.

        Arguments:
        -obj: the Accessible object
        """

        if not obj:
            return -1

        nodes = []
        node = obj
        done = False
        while not done:
            relations = node.getRelationSet()
            node = None
            for relation in relations:
                if relation.getRelationType() \
                       == pyatspi.RELATION_NODE_CHILD_OF:
                    node = relation.getTarget(0)
                    break

            # We want to avoid situations where something gives us an
            # infinite cycle of nodes.  Bon Echo has been seen to do
            # this (see bug 351847).
            #
            if (len(nodes) > 100) or nodes.count(node):
                debug.println(debug.LEVEL_WARNING,
                              "Script.getNodeLevel detected a cycle!!!")
                done = True
            elif node:
                nodes.append(node)
                debug.println(debug.LEVEL_FINEST,
                              "Script.getNodeLevel %d" % len(nodes))
            else:
                done = True

        return len(nodes) - 1

    def getChildNodes(self, obj):
        """Gets all of the children that have RELATION_NODE_CHILD_OF pointing
        to this expanded table cell.

        Arguments:
        -obj: the Accessible Object

        Returns: a list of all the child nodes
        """

        try:
            table = obj.parent.queryTable()
        except:
            return []
        else:
            if not obj.getState().contains(pyatspi.STATE_EXPANDED):
                return []

        nodes = []
        index = self.getCellIndex(obj)
        row = table.getRowAtIndex(index)
        col = table.getColumnAtIndex(index)
        nodeLevel = self.getNodeLevel(obj)
        done = False

        # Candidates will be in the rows beneath the current row.
        # Only check in the current column and stop checking as
        # soon as the node level of a candidate is equal or less
        # than our current level.
        #
        for i in range(row+1, table.nRows):
            cell = table.getAccessibleAt(i, col)
            relations = cell.getRelationSet()
            for relation in relations:
                if relation.getRelationType() \
                       == pyatspi.RELATION_NODE_CHILD_OF:
                    nodeOf = relation.getTarget(0)
                    if self.isSameObject(obj, nodeOf):
                        nodes.append(cell)
                    else:
                        currentLevel = self.getNodeLevel(nodeOf)
                        if currentLevel <= nodeLevel:
                            done = True
                    break
            if done:
                break

        return nodes

    def getKeyBinding(self, obj):
        """Gets the mnemonic, accelerator string and possibly shortcut
        for the given object.  These are based upon the first accessible
        action for the object.

        Arguments:
        - obj: the Accessible object

        Returns: list containing strings: [mnemonic, shortcut, accelerator]
        """

        try:
            action = obj.queryAction()
        except NotImplementedError:
            return ["", "", ""]

        # Action is a string in the format, where the mnemonic and/or
        # accelerator can be missing.
        #
        # <mnemonic>;<full-path>;<accelerator>
        #
        # The keybindings in <full-path> should be separated by ":"
        #
        bindingStrings = action.getKeyBinding(0).decode("UTF-8").split(';')

        if len(bindingStrings) == 3:
            mnemonic       = bindingStrings[0]
            fullShortcut   = bindingStrings[1]
            accelerator    = bindingStrings[2]
        elif len(bindingStrings) > 0:
            mnemonic       = ""
            fullShortcut   = bindingStrings[0]
            try:
                accelerator = bindingStrings[1]
            except:
                accelerator = ""
        else:
            mnemonic       = ""
            fullShortcut   = ""
            accelerator    = ""

        fullShortcut = fullShortcut.replace("<","")
        fullShortcut = fullShortcut.replace(">"," ")
        fullShortcut = fullShortcut.replace(":"," ").strip()

        # If the accelerator or mnemonic strings includes a Space,
        # make sure we speak it.
        #
        if mnemonic.endswith(" "):
            # Translators: this is the spoken word for the space character
            #
            mnemonic += _("space")
        mnemonic = mnemonic.replace("<","")
        mnemonic = mnemonic.replace(">"," ").strip()

        if accelerator.endswith(" "):
            # Translators: this is the spoken word for the space character
            #
            accelerator += _("space")
        accelerator = accelerator.replace("<","")
        accelerator = accelerator.replace(">"," ").strip()

        debug.println(debug.LEVEL_FINEST, "default.getKeyBinding: " \
                      + repr([mnemonic, fullShortcut, accelerator]))

        return [mnemonic, fullShortcut, accelerator]

    def getKnownApplications(self):
        """Retrieves the list of currently running apps for the desktop
        as a list of Accessible objects.
        """

        debug.println(debug.LEVEL_FINEST,
                      "Script.getKnownApplications...")

        apps = filter(lambda x: x is not None,
                      pyatspi.Registry.getDesktop(0))

        debug.println(debug.LEVEL_FINEST,
                      "...Script.getKnownApplications")

        return apps

    def getObjects(self, root, onlyShowing=True):
        """Returns a list of all objects under the given root.  Objects
        are returned in no particular order - this function does a simple
        tree traversal, ignoring the children of objects which report the
        MANAGES_DESCENDANTS state.

        Arguments:
        - root:        the Accessible object to traverse
        - onlyShowing: examine only those objects that are SHOWING

        Returns: a list of all objects under the specified object
        """

        # The list of object we'll return
        #
        objlist = []

        # Start at the first child of the given object
        #
        if root.childCount <= 0:
            return objlist

        for i, child in enumerate(root):
            debug.println(debug.LEVEL_FINEST,
                          "Script.getObjects looking at child %d" % i)
            if child \
               and ((not onlyShowing) or (onlyShowing and \
                    (child.getState().contains(pyatspi.STATE_SHOWING)))):
                objlist.append(child)
                if (child.getState().contains( \
                    pyatspi.STATE_MANAGES_DESCENDANTS) == 0) \
                    and (child.childCount > 0):
                    objlist.extend(self.getObjects(child, onlyShowing))

        return objlist

    def findByRole(self, root, role, onlyShowing=True):
        """Returns a list of all objects of a specific role beneath the
        given root.  [[[TODO: MM - This is very inefficient - this should
        do it's own traversal and not add objects to the list that aren't
        of the specified role.  Instead it uses the traversal from
        getObjects and then deletes objects from the list that aren't of
        the specified role.  Logged as bugzilla bug 319740.]]]

        Arguments:
        - root the Accessible object to traverse
        - role the string describing the Accessible role of the object
        - onlyShowing: examine only those objects that are SHOWING

        Returns a list of descendants of the root that are of the given role.
        """

        objlist = []
        allobjs = self.getObjects(root, onlyShowing)
        for o in allobjs:
            if o.getRole() == role:
                objlist.append(o)
        return objlist

    def findUnrelatedLabels(self, root):
        """Returns a list containing all the unrelated (i.e., have no
        relations to anything and are not a fundamental element of a
        more atomic component like a combo box) labels under the given
        root.  Note that the labels must also be showing on the display.

        Arguments:
        - root the Accessible object to traverse

        Returns a list of unrelated labels under the given root.
        """

        # Find all the labels in the dialog
        #
        allLabels = self.findByRole(root, pyatspi.ROLE_LABEL)

        # add the names of only those labels which are not associated with
        # other objects (i.e., empty relation sets).
        #
        # [[[WDW - HACK: In addition, do not grab free labels whose
        # parents are push buttons because push buttons can have labels as
        # children.]]]
        #
        # [[[WDW - HACK: panels with labelled borders will have a child
        # label that does not have its relation set.  So...we check to see
        # if the panel's name is the same as the label's name.  If so, we
        # ignore the label.]]]
        #
        unrelatedLabels = []

        for label in allLabels:
            relations = label.getRelationSet()
            if len(relations) == 0:
                parent = label.parent
                if parent and (parent.getRole() == pyatspi.ROLE_PUSH_BUTTON):
                    pass
                elif parent and (parent.getRole() == pyatspi.ROLE_PANEL) \
                   and (parent.name == label.name):
                    pass
                elif label.getState().contains(pyatspi.STATE_SHOWING):
                    unrelatedLabels.append(label)

        # Now sort the labels based on their geographic position, top to
        # bottom, left to right.  This is a very inefficient sort, but the
        # assumption here is that there will not be a lot of labels to
        # worry about.
        #
        sortedLabels = []
        for label in unrelatedLabels:
            label_extents = \
                label.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
            index = 0
            for sortedLabel in sortedLabels:
                sorted_extents = \
                    sortedLabel.queryComponent().getExtents(
                  pyatspi.DESKTOP_COORDS)
                if (label_extents.y > sorted_extents.y) \
                   or ((label_extents.y == sorted_extents.y) \
                       and (label_extents.x > sorted_extents.x)):
                    index += 1
                else:
                    break
            sortedLabels.insert(index, label)

        return sortedLabels

    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.decode("UTF-8")):
            if character.isupper():
                voice = settings.voices[settings.UPPERCASE_VOICE]
                character = character.lower()
            else:
                voice =  settings.voices[settings.DEFAULT_VOICE]
            phoneticString = phonnames.getPhoneticName(character)
            speech.speak(phoneticString, voice)

    def printAncestry(self, child):
        """Prints a hierarchical view of a child's ancestry."""

        if not child:
            return

        ancestorList = [child]
        parent = child.parent
        while parent and (parent.parent != parent):
            ancestorList.insert(0, parent)
            parent = parent.parent

        indent = ""
        for ancestor in ancestorList:
            print indent + "+-", debug.getAccessibleDetails(ancestor)
            indent += "  "

    def printHierarchy(self, root, ooi, indent="",
                       onlyShowing=True, omitManaged=True):
        """Prints the accessible hierarchy of all children

        Arguments:
        -indent:      Indentation string
        -root:        Accessible where to start
        -ooi:         Accessible object of interest
        -onlyShowing: If True, only show children painted on the screen
        -omitManaged: If True, omit children that are managed descendants
        """

        if not root:
            return

        if root == ooi:
            print indent + "(*)", debug.getAccessibleDetails(root)
        else:
            print indent + "+-", debug.getAccessibleDetails(root)

        rootManagesDescendants = root.getState().contains( \
                                      pyatspi.STATE_MANAGES_DESCENDANTS)

        for child in root:
            if child == root:
                print indent + "  " + "WARNING CHILD == PARENT!!!"
            elif not child:
                print indent + "  " + "WARNING CHILD IS NONE!!!"
            elif child.parent != root:
                print indent + "  " + "WARNING CHILD.PARENT != PARENT!!!"
            else:
                paint = (not onlyShowing) or (onlyShowing and \
                         child.getState().contains(pyatspi.STATE_SHOWING))
                paint = paint \
                        and ((not omitManaged) \
                             or (omitManaged and not rootManagesDescendants))

                if paint:
                    self.printHierarchy(child,
                                        ooi,
                                        indent + "  ",
                                        onlyShowing,
                                        omitManaged)

    def printApps(self):
        """Prints a list of all applications to stdout."""

        level = debug.LEVEL_OFF

        apps = self.getKnownApplications()
        debug.println(level, "There are %d Accessible applications" % len(apps))
        for app in apps:
            debug.printDetails(level, "  App: ", app, False)
            for child in app:
                debug.printDetails(level, "    Window: ", child, False)
                if child.parent != app:
                    debug.println(level,
                                  "      WARNING: child's parent is not app!!!")

        return True

    def printActiveApp(self):
        """Prints the active application."""

        level = debug.LEVEL_OFF

        window = self.findActiveWindow()
        if not window:
            debug.println(level, "Active application: None")
        else:
            app = window.getApplication()
            if not app:
                debug.println(level, "Active application: None")
            else:
                debug.println(level, "Active application: %s" % app.name)

    def isInActiveApp(self, obj):
        """Returns True if the given object is from the same application that
        currently has keyboard focus.

        Arguments:
        - obj: an Accessible object
        """

        if not obj:
            return False
        else:
            return orca_state.locusOfFocus \
                   and (orca_state.locusOfFocus.getApplication() \
                          == obj.getApplication())

    def findActiveWindow(self):
        """Traverses the list of known apps looking for one who has an
        immediate child (i.e., a window) whose state includes the active state.

        Returns the Python Accessible of the window that's active or None if
        no windows are active.
        """

        window = None
        apps = self.getKnownApplications()
        for app in apps:
            for child in app:
                try:
                    state = child.getState()
                    if state.contains(pyatspi.STATE_ACTIVE):
                        window = child
                        break
                except:
                    debug.printException(debug.LEVEL_FINEST)

        return window

    def getAncestor(self, obj, ancestorRoles, stopRoles):
        """Returns the object of the specified roles which contains the
        given object, or None if the given object is not contained within
        an object the specified roles.

        Arguments:
        - obj: the Accessible object
        - ancestorRoles: the list of roles to look for
        - stopRoles: the list of roles to stop the search at
        """

        if not obj:
            return None

        if not isinstance(ancestorRoles, [].__class__):
            ancestorRoles = [ancestorRoles]

        if not isinstance(stopRoles, [].__class__):
            stopRoles = [stopRoles]

        ancestor = None

        obj = obj.parent
        while obj and (obj != obj.parent):
            if obj.getRole() in ancestorRoles:
                ancestor = obj
                break
            elif obj.getRole() in stopRoles:
                break
            else:
                obj = obj.parent

        return ancestor

    def saveOldAppSettings(self):
        """Save a copy of all the existing application specific settings
        (as specified by the settings.userCustomizableSettings dictionary)."""

        return orca_prefs.readPreferences()

    def restoreOldAppSettings(self, prefsDict):
        """Restore a copy of all the previous saved application settings.

        Arguments:
        - prefsDict: the dictionary containing the old application settings.
        """

        for key in settings.userCustomizableSettings:
            if key in prefsDict:
                setattr(settings, key, prefsDict[key])

    ########################################################################
    #                                                                      #
    # METHODS FOR DRAWING RECTANGLES AROUND OBJECTS ON THE SCREEN          #
    #                                                                      #
    ########################################################################

    def drawOutline(self, x, y, width, height):
        """Draws an outline around the accessible, erasing the
        last drawn outline in the process."""

        if (x == -1) and (y == 0) and (width == 0) and (height == 0):
            outline.erase()
        else:
            outline.draw(x, y, width, height)

    def outlineAccessible(self, accessible):
        """Draws a rectangular outline around the accessible, erasing the
        last drawn rectangle in the process."""

        try:
            component = accessible.queryComponent()
        except AttributeError:
            self.drawOutline(-1, 0, 0, 0)
        except NotImplementedError:
            pass
        else:
            visibleRectangle = \
                component.getExtents(pyatspi.DESKTOP_COORDS)
            self.drawOutline(visibleRectangle.x, visibleRectangle.y,
                             visibleRectangle.width, visibleRectangle.height)

    def isTextSelected(self, obj, startOffset, endOffset):
        """Returns an indication of whether the text is selected by
        comparing the text offset with the various selected regions of
        text for this accessible object.

        Arguments:
        - obj: the Accessible object.
        - startOffset: text start offset.
        - endOffset: text end offset.

        Returns an indication of whether the text is selected.
        """

        # If start offset and end offset are the same, just return False.
        # This is possible if there was no text spoken in onCaretMoved.
        #
        if startOffset == endOffset:
            return False

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

        for i in xrange(text.getNSelections()):
            [startSelOffset, endSelOffset] = text.getSelection(i)
            if (startOffset >= startSelOffset) and (endOffset <= endSelOffset):
                return True

        return False

    def _saveSpokenTextRange(self, startOffset, endOffset):
        """Save away the start and end offset of the range of text that
        was spoken. It will be used by speakTextSelectionState, to try
        to determine if the text was selected or unselected.

        Arguments:
        - startOffset: the start of the spoken text range.
        - endOffset: the end of the spoken text range.
        """

        self.pointOfReference["spokenTextRange"] = [startOffset, endOffset]

    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
        """

        self.pointOfReference["lastCursorPosition"] = [obj, caretOffset]

    def _saveLastTextSelections(self, text):
        """Save away the list of text selections for next time.

        Arguments:
        - text: the text object.
        """

        self.pointOfReference["lastSelections"] = []
        for i in xrange(text.getNSelections()):
            self.pointOfReference["lastSelections"].append(
              text.getSelection(i))

    def speakTextSelectionState(self, obj, startOffset, endOffset):
        """Speak "selected" if the text was just selected, "unselected" if
        it was just unselected.

        Arguments:
        - obj: the Accessible object.
        - startOffset: text start offset.
        - endOffset: text end offset.
        """

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

        # Handle special cases.
        #
        # Control-Shift-Page_Down:
        #          speak "line selected to end from previous cursor position".
        # Control-Shift-Page_Up:
        #        speak "line selected from start to previous cursor position".
        #
        # Shift-Page_Down:    speak "page <state> from cursor position".
        # Shift-Page_Up:      speak "page <state> to cursor position".
        #
        # Control-Shift-Down: speak "line <state> down from cursor position".
        # Control-Shift-Up:   speak "line <state> up from cursor position".
        #
        # Control-Shift-Home: speak "document <state> to cursor position".
        # Control-Shift-End:  speak "document <state> from cursor position".
        #
        # Control-a:          speak "entire document selected".
        #
        # where <state> is either "selected" or "unselected" depending
        # upon whether there are any text selections.
        #
        if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
            eventStr = orca_state.lastNonModifierKeyEvent.event_string
            mods = orca_state.lastInputEvent.modifiers
        else:
            eventStr = None
            mods = 0

        isControlKey = mods & settings.CTRL_MODIFIER_MASK
        isShiftKey = mods & settings.SHIFT_MODIFIER_MASK
        selectedText = (text.getNSelections() != 0)

        specialCaseFound = False
        if (eventStr == "Page_Down") and isShiftKey and isControlKey:
            specialCaseFound = True
            # Translators: when the user selects (highlights) text in
            # a document, Orca will speak information about what they
            # have selected.
            #
            line = _("line selected to end from previous cursor position")

        elif (eventStr == "Page_Up") and isShiftKey and isControlKey:
            specialCaseFound = True
            # Translators: when the user selects (highlights) text in
            # a document, Orca will speak information about what they
            # have selected.
            #
            line = _("line selected from start to previous cursor position")

        elif (eventStr == "Page_Down") and isShiftKey and not isControlKey:
            specialCaseFound = True
            if selectedText:
                # Translators: when the user selects (highlights) text in
                # a document, Orca will speak information about what they
                # have selected.
                #
                line = _("page selected from cursor position")
            else:
                # Translators: when the user unselects text in a document,
                # Orca will speak information about what they have unselected.
                #
                line = _("page unselected from cursor position")

        elif (eventStr == "Page_Up") and isShiftKey and not isControlKey:
            specialCaseFound = True
            if selectedText:
                # Translators: when the user selects (highlights) text in
                # a document, Orca will speak information about what they
                # have selected.
                #
                line = _("page selected to cursor position")
            else:
                # Translators: when the user unselects text in a document,
                # Orca will speak information about what they have unselected.
                #
                line = _("page unselected to cursor position")

        elif (eventStr == "Down") and isShiftKey and isControlKey:
            specialCaseFound = True
            if selectedText:
                # Translators: when the user selects (highlights) text in
                # a document, Orca will speak information about what they
                # have selected.
                #
                line = _("line selected down from cursor position")
            else:
                # Translators: when the user unselects text in a document,
                # Orca will speak information about what they have unselected.
                #
                line = _("line unselected down from cursor position")

        elif (eventStr == "Up") and isShiftKey and isControlKey:
            specialCaseFound = True
            if selectedText:
                # Translators: when the user selects (highlights) text in
                # a document, Orca will speak information about what they
                # have selected.
                #
                line = _("line selected up from cursor position")
            else:
                # Translators: when the user unselects text in a document,
                # Orca will speak information about what they have unselected.
                #
                line = _("line unselected up from cursor position")

        elif (eventStr == "Home") and isShiftKey and isControlKey:
            specialCaseFound = True
            if selectedText:
                # Translators: when the user selects (highlights) text in
                # a document, Orca will speak information about what they
                # have selected.
                #
                line = _("document selected to cursor position")
            else:
                # Translators: when the user unselects text in a document,
                # Orca will speak information about what they have unselected.
                #
                line = _("document unselected to cursor position")

        elif (eventStr == "End") and isShiftKey and isControlKey:
            specialCaseFound = True
            if selectedText:
                # Translators: when the user selects (highlights) text in
                # a document, Orca will speak information about what they
                # have selected.
                #
                line = _("document selected from cursor position")
            else:
                # Translators: when the user unselects text in a document,
                # Orca will speak information about what they have unselected.
                #
                line = _("document unselected from cursor position")

        elif (eventStr == "A") and isControlKey:
            # The user has typed Control-A. Check to see if the entire
            # document has been selected, and if so, let the user know.
            #
            charCount = text.characterCount
            for i in range(0, text.getNSelections()):
                [startOffset, endOffset] = text.getSelection(i)
                if text.caretOffset == 0 and \
                   startOffset == 0 and endOffset == charCount:
                    specialCaseFound = True
                    self.updateBraille(obj)

                    # Translators: this means the user has selected
                    # all the text in a document (e.g., Ctrl+a in gedit).
                    #
                    line = _("entire document selected")

        if specialCaseFound:
            speech.speak(line, None, False)
            return
        elif startOffset == endOffset:
            return

        try:
            # If we are selecting by word, then there possibly will be
            # whitespace characters on either end of the text. We adjust
            # the startOffset and endOffset to exclude them.
            #
            try:
                tmpStr = text.getText(startOffset,
                                      endOffset).decode("UTF-8")
            except:
                tmpStr = u''
            n = len(tmpStr)

            # Don't strip whitespace if string length is one (might be a
            # space).
            #
            if n > 1:
                while endOffset > startOffset:
                    if self.isWordDelimiter(tmpStr[n-1]):
                        n -= 1
                        endOffset -= 1
                    else:
                        break
                n = 0
                while startOffset < endOffset:
                    if self.isWordDelimiter(tmpStr[n]):
                        n += 1
                        startOffset += 1
                    else:
                        break

        except:
            debug.printException(debug.LEVEL_FINEST)

        if self.isTextSelected(obj, startOffset, endOffset):
            # Translators: when the user selects (highlights) text in
            # a document, Orca lets them know this.
            #
            speech.speak(C_("text", "selected"), None, False)
        elif len(text.getText(startOffset, endOffset)):
            # Translators: when the user unselects
            # (unhighlights) text in a document, Orca lets
            # them know this.
            #
            speech.speak(C_("text", "unselected"), None, False)

        self._saveLastTextSelections(text)

    def getURI(self, obj):
        """Return the URI for a given link object.

        Arguments:
        - obj: the Accessible object.
        """
        return obj.queryHyperlink().getURI(0)

    def getDocumentFrame(self):
        """Dummy method used as a reminder to refactor whereamI for links,
        possibly subclassing whereamI for the Gecko script.
        """
        return None

    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 representing
        an error, event etc)
        """
        print "\a"

    def setCaretOffset(self, obj, offset):
        """Set the caret offset on a given accessible. Similar to
        Accessible.setCaretOffset()

        Arguments:
        - obj: Given accessible object.
        - offset: Offset to hich to set the caret.
        """
        try:
            texti = obj.queryText()
        except:
            return None

        texti.setCaretOffset(offset)

    def attribsToDictionary(self, dict_string):
        """Creates a Python dict from a typical attributes list returned from
        different AT-SPI methods.

        Arguments:
        - dict_string: A list of colon seperated key/value pairs seperated by
        semicolons.
        Returns a Python dict of the given attributes.
        """
        try:
            return dict(
                map(lambda pair: pair.strip().split(':'),
                    dict_string.strip('; ').split(';')))
        except ValueError:
            return {}

    def _getPopupItemAtDesktopCoords(self, x, y):
        """Since pop-up items often don't contain nested components, we need
        a way to efficiently determine if the cursor is over a menu item.

        Arguments:
        - x: X coordinate.
        - y: Y coordinate.

        Returns a menu item the mouse is over, or None.
        """
        suspect_children = []
        if self.lastSelectedMenu:
            try:
                si = self.lastSelectedMenu.querySelection()
            except NotImplementedError:
                return None

            if si.nSelectedChildren > 0:
                suspect_children = [si.getSelectedChild(0)]
            else:
                suspect_children = self.lastSelectedMenu
            for child in suspect_children:
                try:
                    ci = child.queryComponent()
                except NotImplementedError:
                    continue

                if ci.contains(x, y, pyatspi.DESKTOP_COORDS) \
                   and ci.getLayer() == pyatspi.LAYER_POPUP:
                    return child

    def getComponentAtDesktopCoords(self, parent, x, y):
        """Get the descendant component at the given desktop coordinates.

        Arguments:

        - parent: The parent component we are searching below.
        - x: X coordinate.
        - y: Y coordinate.

        Returns end-node that contains the given coordinates, or None.
        """
        acc = self._getPopupItemAtDesktopCoords(x, y)
        if acc:
            return acc

        container = parent
        while True:
            if container.getRole() == pyatspi.ROLE_PAGE_TAB_LIST:
                try:
                    si = container.querySelection()
                    container = si.getSelectedChild(0)[0]
                except NotImplementedError:
                    pass
            try:
                ci = container.queryComponent()
            except:
                return None
            else:
                inner_container = container
            container =  ci.getAccessibleAtPoint(x, y, pyatspi.DESKTOP_COORDS)
            if not container or container.queryComponent() == ci:
                # The gecko bridge simply has getAccessibleAtPoint return
                # itself if there are no further children.
                # TODO: Put in Gecko.py
                break
        if inner_container == parent:
            return None
        else:
            return inner_container

    def getTextSelections(self, acc):
        """Get a list of text selections in the given accessible object,
        equivelent to getNSelections()*texti.getSelection()

        Arguments:
        - acc: An accessible.

        Returns list of start and end offsets for multiple selections, or an
        empty list if nothing is selected or if the accessible does not support
        the text interface.
        """
        rv = []
        try:
            texti = acc.queryText()
        except:
            return rv

        for i in xrange(texti.getNSelections()):
            rv.append(texti.getSelection(i))

        return rv

    def speakWordUnderMouse(self, acc):
        """Determine if the speak-word-under-mouse capability applies to
        the given accessible.

        Arguments:
        - acc: Accessible to test.

        Returns True if this accessible should provide the single word.
        """
        return acc and acc.getState().contains(pyatspi.STATE_EDITABLE)

    def getTextAttributes(self, acc, offset, get_defaults=False):
        """Get the text attributes run for a given offset in a given accessible

        Arguments:
        - acc: An accessible.
        - offset: Offset in the accessible's text for which to retrieve the
        attributes.
        - get_defaults: Get the default attributes as well as the unique ones.
        Default is True

        Returns a dictionary of attributes, a start offset where the attributes
        begin, and an end offset. Returns ({}, 0, 0) if the accessible does not
        supprt the text attribute.
        """
        rv = {}
        try:
            texti = acc.queryText()
        except:
            return rv, 0, 0

        if get_defaults:
            rv.update(self.attribsToDictionary(texti.getDefaultAttributes()))

        attrib_str, start, end = texti.getAttributes(offset)

        rv.update(self.attribsToDictionary(attrib_str))

        return rv, start, end

    def getWordAtCoords(self, acc, x, y):
        """Get the word at the given coords in the accessible.

        Arguments:
        - acc: Accessible that supports the Text interface.
        - x: X coordinate.
        - y: Y coordinate.

        Returns a tuple containing the word, start offset, and end offset.
        """
        try:
            ti = acc.queryText()
        except NotImplementedError:
            return '', 0, 0

        text_contents = ti.getText(0, -1)
        line_offsets = []
        start_offset = 0
        while True:
            try:
                end_offset = text_contents.index('\n', start_offset)
            except ValueError:
                line_offsets.append((start_offset, len(text_contents)))
                break
            line_offsets.append((start_offset, end_offset))
            start_offset = end_offset + 1
        for start, end in line_offsets:
            bx, by, bw, bh = \
                ti.getRangeExtents(start, end, pyatspi.DESKTOP_COORDS)
            bb = mouse_review.BoundingBox(bx, by, bw, bh)
            if bb.isInBox(x, y):
                start_offset = 0
                word_offsets = []
                while True:
                    try:
                        end_offset = \
                            text_contents[start:end].index(' ', start_offset)
                    except ValueError:
                        word_offsets.append((start_offset,
                                             len(text_contents[start:end])))
                        break
                    word_offsets.append((start_offset, end_offset))
                    start_offset = end_offset + 1
                for a, b in word_offsets:
                    bx, by, bw, bh = \
                        ti.getRangeExtents(start+a, start+b,
                                           pyatspi.DESKTOP_COORDS)
                    bb = mouse_review.BoundingBox(bx, by, bw, bh)
                    if bb.isInBox(x, y):
                        return text_contents[start+a:start+b], start+a, start+b
        return '', 0, 0

# Dictionary that defines the state changes we care about for various
# objects.  The key represents the role and the value represents a list
# of states that we care about.
#
state_change_notifiers = {}

state_change_notifiers[pyatspi.ROLE_CHECK_MENU_ITEM] = ("checked", None)
state_change_notifiers[pyatspi.ROLE_CHECK_BOX]       = ("checked",
                                                        "indeterminate",
                                                        None)
state_change_notifiers[pyatspi.ROLE_PANEL]           = ("showing", None)
state_change_notifiers[pyatspi.ROLE_LABEL]           = ("showing", None)
state_change_notifiers[pyatspi.ROLE_RADIO_BUTTON]    = ("checked", None)
state_change_notifiers[pyatspi.ROLE_TOGGLE_BUTTON]   = ("checked",
                                                        "pressed",
                                                        None)
state_change_notifiers[pyatspi.ROLE_TABLE_CELL]      = ("checked",
                                                        "expanded",
                                                        None)
state_change_notifiers[pyatspi.ROLE_LIST_ITEM]       = ("expanded", None)
