[orca] Revert "Create "web" script and use it for Gecko content"



commit e36355ba6e27bbda0061fc715bd27f29e7afcb73
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Fri Jun 12 00:31:15 2015 -0400

    Revert "Create "web" script and use it for Gecko content"
    
    Caught a regression in Thunderbird. Reverting until I fix it.

 configure.ac                                       |    1 -
 po/POTFILES.in                                     |    1 +
 src/orca/script_manager.py                         |   10 +-
 src/orca/script_utilities.py                       |    3 +-
 src/orca/scripts/Makefile.am                       |    2 +-
 .../scripts/apps/Thunderbird/speech_generator.py   |   47 +-
 src/orca/scripts/toolkits/Gecko/Makefile.am        |    6 +-
 src/orca/scripts/toolkits/Gecko/__init__.py        |    3 +
 src/orca/scripts/toolkits/Gecko/bookmarks.py       |  209 +++
 .../{web => toolkits/Gecko}/braille_generator.py   |   10 +-
 src/orca/scripts/toolkits/Gecko/script.py          | 1628 ++++++++++++++++-
 .../scripts/toolkits/Gecko/script_utilities.py     | 1804 +++++++++++++++++++-
 .../{web => toolkits/Gecko}/speech_generator.py    |   16 +-
 .../{web => toolkits/Gecko}/tutorial_generator.py  |   12 +-
 src/orca/scripts/web/Makefile.am                   |   10 -
 src/orca/scripts/web/__init__.py                   |   26 -
 src/orca/scripts/web/bookmarks.py                  |  139 --
 src/orca/scripts/web/script.py                     | 1529 ----------------
 src/orca/scripts/web/script_utilities.py           | 1851 --------------------
 19 files changed, 3627 insertions(+), 3680 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index fdecc3a..58ca5c4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -123,7 +123,6 @@ src/orca/scripts/apps/soffice/Makefile
 src/orca/scripts/apps/Thunderbird/Makefile
 src/orca/scripts/apps/xfwm4/Makefile
 src/orca/scripts/toolkits/Makefile
-src/orca/scripts/web/Makefile
 src/orca/scripts/toolkits/Gecko/Makefile
 src/orca/scripts/toolkits/J2SE-access-bridge/Makefile
 src/orca/scripts/toolkits/clutter/Makefile
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 3ac2111..7bc77f2 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -23,6 +23,7 @@ src/orca/scripts/apps/liferea/script.py
 src/orca/scripts/apps/metacity/script.py
 src/orca/scripts/apps/planner/braille_generator.py
 src/orca/scripts/apps/planner/speech_generator.py
+src/orca/scripts/apps/Thunderbird/speech_generator.py
 src/orca/scripts/default.py
 src/orca/text_attribute_names.py
 src/orca/tutorialgenerator.py
diff --git a/src/orca/script_manager.py b/src/orca/script_manager.py
index 9536a2c..4078c27 100644
--- a/src/orca/script_manager.py
+++ b/src/orca/script_manager.py
@@ -146,28 +146,28 @@ class ScriptManager:
         script = None
         for package in self._scriptPackages:
             moduleName = '.'.join((package, name))
-            debug.println(debug.LEVEL_FINE, "Looking for %s" % moduleName)
+            debug.println(debug.LEVEL_FINE, "Looking for %s.py" % moduleName)
             try:
                 module = importlib.import_module(moduleName)
             except ImportError:
                 debug.println(
-                    debug.LEVEL_FINE, "Could not import %s" % moduleName)
+                    debug.LEVEL_FINE, "Could not import %s.py" % moduleName)
                 continue
             except OSError:
                 debug.examineProcesses()
 
-            debug.println(debug.LEVEL_FINE, "Found %s" % moduleName)
+            debug.println(debug.LEVEL_FINE, "Found %s.py" % moduleName)
             try:
                 if hasattr(module, 'getScript'):
                     script = module.getScript(app)
                 else:
                     script = module.Script(app)
-                debug.println(debug.LEVEL_FINE, "Loaded %s" % moduleName)
+                debug.println(debug.LEVEL_FINE, "Loaded %s.py" % moduleName)
                 break
             except:
                 debug.printException(debug.LEVEL_FINEST)
                 debug.println(
-                    debug.LEVEL_FINEST, "Could not load %s" % moduleName)
+                    debug.LEVEL_FINEST, "Could not load %s.py" % moduleName)
 
         return script
 
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index bfdfbb0..69c11be 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -1599,7 +1599,8 @@ class Utilities:
 
         return rv
 
-    def characterOffsetInParent(self, obj):
+    @staticmethod
+    def characterOffsetInParent(obj):
         """Returns the character offset of the embedded object
         character for this object in its parent's accessible text.
 
diff --git a/src/orca/scripts/Makefile.am b/src/orca/scripts/Makefile.am
index bd5c6c3..49b289a 100644
--- a/src/orca/scripts/Makefile.am
+++ b/src/orca/scripts/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = apps toolkits web
+SUBDIRS = apps toolkits
 
 orca_python_PYTHON = \
        __init__.py \
diff --git a/src/orca/scripts/apps/Thunderbird/speech_generator.py 
b/src/orca/scripts/apps/Thunderbird/speech_generator.py
index bb56f22..2c7b262 100644
--- a/src/orca/scripts/apps/Thunderbird/speech_generator.py
+++ b/src/orca/scripts/apps/Thunderbird/speech_generator.py
@@ -17,7 +17,8 @@
 # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
 # Boston MA  02110-1301 USA.
 
-"""Custom script for Thunderbird"""
+""" Custom script for Thunderbird 3.
+"""
 
 __id__        = "$Id$"
 __version__   = "$Revision$"
@@ -27,14 +28,24 @@ __license__   = "LGPL"
 
 import pyatspi
 
-from orca import speech_generator
+import orca.scripts.toolkits.Gecko as Gecko
 
+from orca.orca_i18n import _
 
-class SpeechGenerator(speech_generator.SpeechGenerator):
-    """Provides a speech generator specific to Thunderbird."""
+########################################################################
+#                                                                      #
+# Custom SpeechGenerator for Thunderbird                               #
+#                                                                      #
+########################################################################
+
+class SpeechGenerator(Gecko.SpeechGenerator):
+    """Provides a speech generator specific to Thunderbird.
+    """
+
+    # pylint: disable-msg=W0142
 
     def __init__(self, script):
-        super().__init__(script)
+        Gecko.SpeechGenerator.__init__(self, script)
 
     def _generateColumnHeader(self, obj, **args):
         """Returns an array of strings (and possibly voice and audio
@@ -42,7 +53,31 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
         that is in a table, if it exists.  Otherwise, an empty array
         is returned.
         """
+        result = []
 
         # Don't speak Thunderbird column headers, since
         # it's not possible to navigate across a row.
-        return []
+        #
+        return result
+
+    def _generateUnrelatedLabels(self, obj, **args):
+        """Finds all labels not in a label for or labelled by relation.
+        If this is the spell checking dialog, then there are no
+        unrelated labels.  See bug #535192 for more details.
+        """
+        result = []
+
+        # Translators: this is what the name of the spell checking
+        # dialog in Thunderbird begins with. The translated form
+        # has to match what Thunderbird is using.  We hate keying
+        # off stuff like this, but we're forced to do so in this case.
+        #
+        if obj.name.startswith(_("Check Spelling")) \
+           and self._script.utilities.hasMatchingHierarchy(
+                   obj, [pyatspi.ROLE_DIALOG,
+                         pyatspi.ROLE_APPLICATION]):
+            pass
+        else:
+            result.extend(Gecko.SpeechGenerator._generateUnrelatedLabels(
+                              self, obj, **args))
+        return result
diff --git a/src/orca/scripts/toolkits/Gecko/Makefile.am b/src/orca/scripts/toolkits/Gecko/Makefile.am
index bf85b9b..0d76502 100644
--- a/src/orca/scripts/toolkits/Gecko/Makefile.am
+++ b/src/orca/scripts/toolkits/Gecko/Makefile.am
@@ -1,7 +1,11 @@
 orca_python_PYTHON = \
        __init__.py \
+       bookmarks.py \
+       braille_generator.py \
        script.py \
-       script_utilities.py
+       script_utilities.py \
+       speech_generator.py \
+       tutorial_generator.py
 
 orca_pythondir=$(pkgpythondir)/scripts/toolkits/Gecko
 
diff --git a/src/orca/scripts/toolkits/Gecko/__init__.py b/src/orca/scripts/toolkits/Gecko/__init__.py
index a1ec518..4209fcd 100644
--- a/src/orca/scripts/toolkits/Gecko/__init__.py
+++ b/src/orca/scripts/toolkits/Gecko/__init__.py
@@ -1,2 +1,5 @@
 from .script import Script
+from .speech_generator import SpeechGenerator
+from .braille_generator import BrailleGenerator
 from .script_utilities import Utilities
+from .tutorial_generator import TutorialGenerator
diff --git a/src/orca/scripts/toolkits/Gecko/bookmarks.py b/src/orca/scripts/toolkits/Gecko/bookmarks.py
new file mode 100644
index 0000000..b29d017
--- /dev/null
+++ b/src/orca/scripts/toolkits/Gecko/bookmarks.py
@@ -0,0 +1,209 @@
+# Orca
+#
+# Copyright 2005-2008 Sun Microsystems Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+"""Custom script for Gecko toolkit.
+Please refer to the following URL for more information on the AT-SPI
+implementation in Gecko:
+http://developer.mozilla.org/en/docs/Accessibility/ATSPI_Support
+"""
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc."
+__license__   = "LGPL"
+
+import pyatspi
+
+import orca.bookmarks as bookmarks
+import orca.messages as messages
+
+####################################################################
+#                                                                  #
+# Custom bookmarks class                                           #
+#                                                                  #
+####################################################################
+class GeckoBookmarks(bookmarks.Bookmarks):
+    def __init__(self, script):
+        bookmarks.Bookmarks.__init__(self, script)
+        self._currentbookmarkindex = {}
+        
+        
+    def addBookmark(self, inputEvent):
+        """ Add an in-page accessible object bookmark for this key and
+        webpage URI. """ 
+        # form bookmark dictionary key
+        index = (inputEvent.hw_code, self.getURIKey())
+        # convert the current object to a path and bookmark it
+        obj, characterOffset = self._script.utilities.getCaretContext()
+        path = self._objToPath()
+        self._bookmarks[index] = path, characterOffset
+        self._script.presentMessage(messages.BOOKMARK_ENTERED)
+        
+    def goToBookmark(self, inputEvent, index=None):
+        """ Go to the bookmark indexed at this key and this page's URI """
+        index = index or (inputEvent.hw_code, self.getURIKey())
+        
+        try:
+            path, characterOffset = self._bookmarks[index]
+        except KeyError:
+            self._script.systemBeep()
+            return
+        # convert our path to an object
+        obj = self.pathToObj(path)
+       
+        if obj:
+            # restore the location
+            self._script.utilities.setCaretPosition(obj, characterOffset)
+            self._script.updateBraille(obj)
+            self._script.speakContents( \
+                self._script.utilities.getObjectContentsAtOffset(obj, characterOffset))
+            # update the currentbookmark
+            self._currentbookmarkindex[index[1]] = index[0]
+        else:
+            self._script.systemBeep()
+        
+    def bookmarkCurrentWhereAmI(self, inputEvent):
+        """ Report "Where am I" information for this bookmark relative to the 
+        current pointer location."""
+        index = (inputEvent.hw_code, self.getURIKey())
+        try:
+            path, characterOffset = self._bookmarks[index]
+            obj = self.pathToObj(path)
+        except KeyError:
+            self._script.systemBeep()
+            return
+            
+        [cur_obj, cur_characterOffset] = self._script.utilities.getCaretContext()
+        
+        # Are they the same object?
+        if self._script.utilities.isSameObject(cur_obj, obj):
+            self._script.presentMessage(messages.BOOKMARK_IS_CURRENT_OBJECT)
+            return
+        # Are their parents the same?
+        elif self._script.utilities.isSameObject(cur_obj.parent, obj.parent):
+            self._script.presentMessage(messages.BOOKMARK_PARENT_IS_SAME)
+            return
+        
+        # Do they share a common ancestor?
+        # bookmark's ancestors
+        bookmark_ancestors = []
+        p = obj.parent
+        while p:
+            bookmark_ancestors.append(p)
+            p = p.parent
+        # look at current object's ancestors to compare to bookmark's ancestors
+        p = cur_obj.parent
+        while p:
+            if bookmark_ancestors.count(p) > 0:
+                rolename = p.getLocalizedRoleName()
+                self._script.presentMessage(
+                    messages.BOOKMARK_SHARED_ANCESTOR % rolename)
+                return
+            p = p.parent
+
+        self._script.presentMessage(messages.BOOKMARK_COMPARISON_UNKNOWN)
+        
+    def saveBookmarks(self, inputEvent):
+        """ Save the bookmarks for this script. """
+        saved = {}
+         
+        # save obj as a path instead of an accessible
+        for index, bookmark in list(self._bookmarks.items()):
+            saved[index] = bookmark[0], bookmark[1]
+            
+        try:
+            self.saveBookmarksToDisk(saved)
+            self._script.presentMessage(messages.BOOKMARKS_SAVED)
+        except IOError:
+            self._script.presentMessage(messages.BOOKMARKS_SAVED_FAILURE)
+
+        # Notify the observers
+        for o in self._saveObservers:
+            o()
+            
+    def goToNextBookmark(self, inputEvent):
+        """ Go to the next bookmark location.  If no bookmark has yet to be
+        selected, the first bookmark will be used.  """
+        # The convenience of using a dictionary to add/goto a bookmark is 
+        # offset by the difficulty in finding the next bookmark.  We will 
+        # need to sort our keys to determine the next bookmark on a page by 
+        # page basis.
+        bm_keys = list(self._bookmarks.keys())
+        current_uri = self.getURIKey()
+        
+        # mine out the hardware keys for this page and sort them
+        thispage_hwkeys = []
+        for bm_key in bm_keys:
+            if bm_key[1] == current_uri:
+                thispage_hwkeys.append(bm_key[0])
+        thispage_hwkeys.sort()
+        
+        # no bookmarks for this page
+        if len(thispage_hwkeys) == 0:
+            self._script.systemBeep()
+            return
+        # only 1 bookmark or we are just starting out
+        elif len(thispage_hwkeys) == 1 or \
+                         current_uri not in self._currentbookmarkindex:
+            self.goToBookmark(None, index=(thispage_hwkeys[0], current_uri))
+            return
+        
+        # find current bookmark hw_code in our sorted list.  
+        # Go to next one if possible
+        try:
+            index = thispage_hwkeys.index( \
+                                 self._currentbookmarkindex[current_uri])
+            self.goToBookmark(None, index=( \
+                                 thispage_hwkeys[index+1], current_uri))
+        except (ValueError, KeyError, IndexError):
+            self.goToBookmark(None, index=(thispage_hwkeys[0], current_uri))
+            
+    def goToPrevBookmark(self, inputEvent):
+        """ Go to the previous bookmark location.  If no bookmark has yet to be
+        selected, the first bookmark will be used.  """
+        bm_keys = list(self._bookmarks.keys())
+        current_uri = self.getURIKey()
+        
+        # mine out the hardware keys for this page and sort them
+        thispage_hwkeys = []
+        for bm_key in bm_keys:
+            if bm_key[1] == current_uri:
+                thispage_hwkeys.append(bm_key[0])
+        thispage_hwkeys.sort()
+        
+        # no bookmarks for this page
+        if len(thispage_hwkeys) == 0:
+            self._script.systemBeep()
+            return
+        # only 1 bookmark or we are just starting out
+        elif len(thispage_hwkeys) == 1 or \
+                         current_uri not in self._currentbookmarkindex:
+            self.goToBookmark(None, index=(thispage_hwkeys[0], current_uri))
+            return
+        
+        # find current bookmark hw_code in our sorted list.  
+        # Go to next one if possible
+        try:
+            index = thispage_hwkeys.index( \
+                            self._currentbookmarkindex[current_uri])
+            self.goToBookmark(None, 
+                              index=(thispage_hwkeys[index-1], current_uri))
+        except (ValueError, KeyError, IndexError):
+            self.goToBookmark(None, index=(thispage_hwkeys[0], current_uri))    
diff --git a/src/orca/scripts/web/braille_generator.py b/src/orca/scripts/toolkits/Gecko/braille_generator.py
similarity index 95%
rename from src/orca/scripts/web/braille_generator.py
rename to src/orca/scripts/toolkits/Gecko/braille_generator.py
index 74b176e..9d3ab63 100644
--- a/src/orca/scripts/web/braille_generator.py
+++ b/src/orca/scripts/toolkits/Gecko/braille_generator.py
@@ -35,7 +35,6 @@ from orca import messages
 from orca import object_properties
 from orca import orca_state
 
-
 class BrailleGenerator(braille_generator.BrailleGenerator):
 
     def __init__(self, script):
@@ -109,7 +108,6 @@ class BrailleGenerator(braille_generator.BrailleGenerator):
         text = self._script.utilities.expandEOCs(obj, startOffset, endOffset)
         if text:
             result.append(text)
-
         return result
 
     def generateBraille(self, obj, **args):
@@ -123,16 +121,20 @@ class BrailleGenerator(braille_generator.BrailleGenerator):
         elif self._script.utilities.isStatic(obj):
             oldRole = self._overrideRole('ROLE_STATIC', args)
 
+        # Treat menu items in collapsed combo boxes as if the combo box
+        # had focus. This will make things more consistent with how we
+        # present combo boxes outside of Gecko.
+        #
         if obj.getRole() == pyatspi.ROLE_MENU_ITEM:
             comboBox = self._script.utilities.ancestorWithRole(
                 obj, [pyatspi.ROLE_COMBO_BOX], [pyatspi.ROLE_FRAME])
-            if comboBox and not comboBox.getState().contains(pyatspi.STATE_EXPANDED):
+            if comboBox \
+               and not comboBox.getState().contains(pyatspi.STATE_EXPANDED):
                 obj = comboBox
         result.extend(super().generateBraille(obj, **args))
         del args['includeContext']
         if oldRole:
             self._restoreRole(oldRole, args)
-
         return result
 
     def _generateEol(self, obj, **args):
diff --git a/src/orca/scripts/toolkits/Gecko/script.py b/src/orca/scripts/toolkits/Gecko/script.py
index 07c8b32..152ba89 100644
--- a/src/orca/scripts/toolkits/Gecko/script.py
+++ b/src/orca/scripts/toolkits/Gecko/script.py
@@ -19,6 +19,17 @@
 # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
 # Boston MA  02110-1301 USA.
 
+# [[[TODO: WDW - Pylint is giving us a bunch of errors along these
+# lines throughout this file:
+#
+# E1103:4241:Script.updateBraille: Instance of 'list' has no 'getRole'
+# member (but some types could not be inferred)
+#
+# I don't know what is going on, so I'm going to tell pylint to
+# disable those messages for Gecko.py.]]]
+#
+# pylint: disable-msg=E1103
+
 __id__        = "$Id$"
 __version__   = "$Revision$"
 __date__      = "$Date$"
@@ -27,22 +38,107 @@ __copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." \
                 "Copyright (c) 2014-2015 Igalia, S.L."
 __license__   = "LGPL"
 
+from gi.repository import Gtk
 import pyatspi
-
-from orca import debug
-from orca import orca
-from orca.scripts import default
-from orca.scripts import web
+import time
+
+import orca.braille as braille
+import orca.caret_navigation as caret_navigation
+import orca.cmdnames as cmdnames
+import orca.debug as debug
+import orca.scripts.default as default
+import orca.eventsynthesizer as eventsynthesizer
+import orca.guilabels as guilabels
+import orca.input_event as input_event
+import orca.keybindings as keybindings
+import orca.liveregions as liveregions
+import orca.messages as messages
+import orca.orca as orca
+import orca.orca_state as orca_state
+import orca.settings as settings
+import orca.settings_manager as settings_manager
+import orca.speech as speech
+import orca.speechserver as speechserver
+import orca.structural_navigation as structural_navigation
+
+from .braille_generator import BrailleGenerator
+from .speech_generator import SpeechGenerator
+from .bookmarks import GeckoBookmarks
 from .script_utilities import Utilities
+from .tutorial_generator import TutorialGenerator
 
+from orca.acss import ACSS
 
-class Script(web.Script):
+_settingsManager = settings_manager.getManager()
+
+########################################################################
+#                                                                      #
+# Script                                                               #
+#                                                                      #
+########################################################################
+
+class Script(default.Script):
+    """The script for Firefox."""
+
+    ####################################################################
+    #                                                                  #
+    # Overridden Script Methods                                        #
+    #                                                                  #
+    ####################################################################
 
     def __init__(self, app):
-        super().__init__(app)
+        default.Script.__init__(self, app)
+        # Initialize variables to make pylint happy.
+        #
+        self.changedLinesOnlyCheckButton = None
+        self.controlCaretNavigationCheckButton = None
+        self.minimumFindLengthAdjustment = None
+        self.minimumFindLengthLabel = None
+        self.minimumFindLengthSpinButton = None
+        self.sayAllOnLoadCheckButton = None
+        self.skipBlankCellsCheckButton = None
+        self.speakCellCoordinatesCheckButton = None
+        self.speakCellHeadersCheckButton = None
+        self.speakCellSpanCheckButton = None
+        self.speakResultsDuringFindCheckButton = None
+        self.structuralNavigationCheckButton = None
+        self.autoFocusModeStructNavCheckButton = None
+        self.autoFocusModeCaretNavCheckButton = None
+        self.layoutModeCheckButton = None
+
+        if _settingsManager.getSetting('caretNavigationEnabled') == None:
+            _settingsManager.setSetting('caretNavigationEnabled', True)
+        if _settingsManager.getSetting('sayAllOnLoad') == None:
+            _settingsManager.setSetting('sayAllOnLoad', True)
+
+        # We keep track of whether we're currently in the process of
+        # loading a page.
+        #
+        self._loadingDocumentContent = False
 
-        # TODO - JD: This should also not be needed. In theory, they've
-        # converted to the new attribute styles.
+        # In tabbed content (i.e., Firefox's support for one tab per
+        # URL), we also keep track of the caret context in each tab.
+        # the key is the document frame and the value is the caret
+        # context for that frame.
+        #
+        self._documentFrameCaretContext = {}
+
+        # During a find we get caret-moved events reflecting the changing
+        # screen contents.  The user can opt to have these changes announced.
+        # If the announcement is enabled, it still only will be made if the
+        # selected text is a certain length (user-configurable) and if the
+        # line has changed (so we don't keep repeating the line).  However,
+        # the line has almost certainly changed prior to this length being
+        # reached.  Therefore, we need to make an initial announcement, which
+        # means we need to know if that has already taken place.
+        #
+        self._madeFindAnnouncement = False
+
+        # For really large objects, a call to getAttributes can take up to
+        # two seconds! This is a Firefox bug. We'll try to improve things
+        # by storing attributes.
+        #
+        self.currentAttrs = {}
 
         # A dictionary of Gecko-style attribute names and their equivalent/
         # expected names. This is necessary so that we can present the
@@ -76,20 +172,574 @@ class Script(web.Script):
             "underlinesolid"          : "single",
             "line-throughsolid"       : "solid"}
 
+        # Keep track of the last object which appeared as a result of
+        # the user routing the mouse pointer over an object. Also keep
+        # track of the object which is associated with the mouse over
+        # so that we can restore focus to it if need be.
+        #
+        self.lastMouseOverObject = None
+        self.preMouseOverContext = [None, -1]
+        self.inMouseOverObject = False
+
+        self._inFocusMode = False
+        self._focusModeIsSticky = False
+
+        self._lastCommandWasCaretNav = False
+        self._lastCommandWasStructNav = False
+        self._lastCommandWasMouseButton = False
+
+        self._sayAllContents = []
+
+        # See bug 665522 - comment 5 regarding children. We're also seeing
+        # stale names in both Gecko and other toolkits.
+        app.setCacheMask(
+            pyatspi.cache.DEFAULT ^ pyatspi.cache.CHILDREN ^ pyatspi.cache.NAME)
+
+    def deactivate(self):
+        """Called when this script is deactivated."""
+
+        self._sayAllContents = []
+        self._inSayAll = False
+        self._sayAllIsInterrupted = False
+        self._loadingDocumentContent = False
+        self._madeFindAnnouncement = False
+        self._lastCommandWasCaretNav = False
+        self._lastCommandWasStructNav = False
+        self._lastCommandWasMouseButton = False
+        self._lastMouseOverObject = None
+        self._preMouseOverContext = None, -1
+        self._inMouseOverObject = False
+        self.utilities.clearCachedObjects()
+
+    def getBookmarks(self):
+        """Returns the "bookmarks" class for this script.
+        """
+        try:
+            return self.bookmarks
+        except AttributeError:
+            self.bookmarks = GeckoBookmarks(self)
+            return self.bookmarks
+
+    def getBrailleGenerator(self):
+        """Returns the braille generator for this script.
+        """
+        return BrailleGenerator(self)
+
+    def getSpeechGenerator(self):
+        """Returns the speech generator for this script.
+        """
+        return SpeechGenerator(self)
+
+    def getTutorialGenerator(self):
+        """Returns the tutorial generator for this script."""
+        return TutorialGenerator(self)
+
     def getUtilities(self):
         """Returns the utilites for this script."""
 
         return Utilities(self)
 
-    def locusOfFocusChanged(self, event, oldFocus, newFocus):
-        """Handles changes of focus of interest to the script."""
+    def getEnabledStructuralNavigationTypes(self):
+        """Returns a list of the structural navigation object types
+        enabled in this script.
+        """
 
-        if super().locusOfFocusChanged(event, oldFocus, newFocus):
+        return [structural_navigation.StructuralNavigation.BLOCKQUOTE,
+                structural_navigation.StructuralNavigation.BUTTON,
+                structural_navigation.StructuralNavigation.CHECK_BOX,
+                structural_navigation.StructuralNavigation.CHUNK,
+                structural_navigation.StructuralNavigation.CLICKABLE,
+                structural_navigation.StructuralNavigation.COMBO_BOX,
+                structural_navigation.StructuralNavigation.ENTRY,
+                structural_navigation.StructuralNavigation.FORM_FIELD,
+                structural_navigation.StructuralNavigation.HEADING,
+                structural_navigation.StructuralNavigation.IMAGE,
+                structural_navigation.StructuralNavigation.LANDMARK,
+                structural_navigation.StructuralNavigation.LINK,
+                structural_navigation.StructuralNavigation.LIST,
+                structural_navigation.StructuralNavigation.LIST_ITEM,
+                structural_navigation.StructuralNavigation.LIVE_REGION,
+                structural_navigation.StructuralNavigation.PARAGRAPH,
+                structural_navigation.StructuralNavigation.RADIO_BUTTON,
+                structural_navigation.StructuralNavigation.SEPARATOR,
+                structural_navigation.StructuralNavigation.TABLE,
+                structural_navigation.StructuralNavigation.TABLE_CELL,
+                structural_navigation.StructuralNavigation.UNVISITED_LINK,
+                structural_navigation.StructuralNavigation.VISITED_LINK]
+
+    def getLiveRegionManager(self):
+        """Returns the live region support for this script."""
+
+        return liveregions.LiveRegionManager(self)
+
+    def getCaretNavigation(self):
+        """Returns the caret navigation support for this script."""
+
+        return caret_navigation.CaretNavigation(self)
+
+    def setupInputEventHandlers(self):
+        """Defines InputEventHandlers for this script."""
+
+        super().setupInputEventHandlers()
+        self.inputEventHandlers.update(
+            self.structuralNavigation.inputEventHandlers)
+
+        self.inputEventHandlers.update(
+            self.caretNavigation.get_handlers())
+
+        self.inputEventHandlers.update(
+            self.liveRegionManager.inputEventHandlers)
+
+        self.inputEventHandlers["sayAllHandler"] = \
+            input_event.InputEventHandler(
+                Script.sayAll,
+                cmdnames.SAY_ALL)
+
+        self.inputEventHandlers["panBrailleLeftHandler"] = \
+            input_event.InputEventHandler(
+                Script.panBrailleLeft,
+                cmdnames.PAN_BRAILLE_LEFT,
+                False) # Do not enable learn mode for this action
+
+        self.inputEventHandlers["panBrailleRightHandler"] = \
+            input_event.InputEventHandler(
+                Script.panBrailleRight,
+                cmdnames.PAN_BRAILLE_RIGHT,
+                False) # Do not enable learn mode for this action
+
+        self.inputEventHandlers["moveToMouseOverHandler"] = \
+            input_event.InputEventHandler(
+                Script.moveToMouseOver,
+                cmdnames.MOUSE_OVER_MOVE)
+
+        self.inputEventHandlers["togglePresentationModeHandler"] = \
+            input_event.InputEventHandler(
+                Script.togglePresentationMode,
+                cmdnames.TOGGLE_PRESENTATION_MODE)
+
+        self.inputEventHandlers["enableStickyFocusModeHandler"] = \
+            input_event.InputEventHandler(
+                Script.enableStickyFocusMode,
+                cmdnames.SET_FOCUS_MODE_STICKY)
+
+    def getToolkitKeyBindings(self):
+        """Returns the toolkit-specific keybindings for this script."""
+
+        keyBindings = keybindings.KeyBindings()
+
+        structNavBindings = self.structuralNavigation.keyBindings
+        for keyBinding in structNavBindings.keyBindings:
+            keyBindings.add(keyBinding)
+
+        caretNavBindings = self.caretNavigation.get_bindings()
+        for keyBinding in caretNavBindings.keyBindings:
+            keyBindings.add(keyBinding)
+
+        liveRegionBindings = self.liveRegionManager.keyBindings
+        for keyBinding in liveRegionBindings.keyBindings:
+            keyBindings.add(keyBinding)
+
+        keyBindings.add(
+            keybindings.KeyBinding(
+                "a",
+                keybindings.defaultModifierMask,
+                keybindings.ORCA_MODIFIER_MASK,
+                self.inputEventHandlers.get("togglePresentationModeHandler")))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
+                "a",
+                keybindings.defaultModifierMask,
+                keybindings.ORCA_MODIFIER_MASK,
+                self.inputEventHandlers.get("enableStickyFocusModeHandler"),
+                2))
+
+        layout = _settingsManager.getSetting('keyboardLayout')
+        if layout == settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP:
+            key = "KP_Multiply"
+        else:
+            key = "0"
+
+        keyBindings.add(
+            keybindings.KeyBinding(
+                key,
+                keybindings.defaultModifierMask,
+                keybindings.ORCA_MODIFIER_MASK,
+                self.inputEventHandlers.get("moveToMouseOverHandler")))
+
+        return keyBindings
+
+    def getAppPreferencesGUI(self):
+        """Return a GtkGrid containing the application unique configuration
+        GUI items for the current application."""
+
+        grid = Gtk.Grid()
+        grid.set_border_width(12)
+
+        generalFrame = Gtk.Frame()
+        grid.attach(generalFrame, 0, 0, 1, 1)
+
+        label = Gtk.Label(label="<b>%s</b>" % guilabels.PAGE_NAVIGATION)
+        label.set_use_markup(True)
+        generalFrame.set_label_widget(label)
+
+        generalAlignment = Gtk.Alignment.new(0.5, 0.5, 1, 1)
+        generalAlignment.set_padding(0, 0, 12, 0)
+        generalFrame.add(generalAlignment)
+        generalGrid = Gtk.Grid()
+        generalAlignment.add(generalGrid)
+
+        label = guilabels.USE_CARET_NAVIGATION
+        value = _settingsManager.getSetting('caretNavigationEnabled')
+        self.controlCaretNavigationCheckButton = \
+            Gtk.CheckButton.new_with_mnemonic(label)
+        self.controlCaretNavigationCheckButton.set_active(value) 
+        generalGrid.attach(self.controlCaretNavigationCheckButton, 0, 0, 1, 1)
+
+        label = guilabels.AUTO_FOCUS_MODE_CARET_NAV
+        value = _settingsManager.getSetting('caretNavTriggersFocusMode')
+        self.autoFocusModeCaretNavCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
+        self.autoFocusModeCaretNavCheckButton.set_active(value)
+        generalGrid.attach(self.autoFocusModeCaretNavCheckButton, 0, 1, 1, 1)
+
+        label = guilabels.USE_STRUCTURAL_NAVIGATION
+        value = self.structuralNavigation.enabled
+        self.structuralNavigationCheckButton = \
+            Gtk.CheckButton.new_with_mnemonic(label)
+        self.structuralNavigationCheckButton.set_active(value)
+        generalGrid.attach(self.structuralNavigationCheckButton, 0, 2, 1, 1)
+
+        label = guilabels.AUTO_FOCUS_MODE_STRUCT_NAV
+        value = _settingsManager.getSetting('structNavTriggersFocusMode')
+        self.autoFocusModeStructNavCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
+        self.autoFocusModeStructNavCheckButton.set_active(value)
+        generalGrid.attach(self.autoFocusModeStructNavCheckButton, 0, 3, 1, 1)
+
+        label = guilabels.READ_PAGE_UPON_LOAD
+        value = _settingsManager.getSetting('sayAllOnLoad')
+        self.sayAllOnLoadCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
+        self.sayAllOnLoadCheckButton.set_active(value)
+        generalGrid.attach(self.sayAllOnLoadCheckButton, 0, 4, 1, 1)
+
+        label = guilabels.CONTENT_LAYOUT_MODE
+        value = _settingsManager.getSetting('layoutMode')
+        self.layoutModeCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
+        self.layoutModeCheckButton.set_active(value)
+        generalGrid.attach(self.layoutModeCheckButton, 0, 5, 1, 1)
+
+        tableFrame = Gtk.Frame()
+        grid.attach(tableFrame, 0, 1, 1, 1)
+
+        label = Gtk.Label(label="<b>%s</b>" % guilabels.TABLE_NAVIGATION)
+        label.set_use_markup(True)
+        tableFrame.set_label_widget(label)
+
+        tableAlignment = Gtk.Alignment.new(0.5, 0.5, 1, 1)
+        tableAlignment.set_padding(0, 0, 12, 0)
+        tableFrame.add(tableAlignment)
+        tableGrid = Gtk.Grid()
+        tableAlignment.add(tableGrid)
+
+        label = guilabels.TABLE_SPEAK_CELL_COORDINATES
+        value = _settingsManager.getSetting('speakCellCoordinates')
+        self.speakCellCoordinatesCheckButton = \
+            Gtk.CheckButton.new_with_mnemonic(label)
+        self.speakCellCoordinatesCheckButton.set_active(value)
+        tableGrid.attach(self.speakCellCoordinatesCheckButton, 0, 0, 1, 1)
+
+        label = guilabels.TABLE_SPEAK_CELL_SPANS
+        value = _settingsManager.getSetting('speakCellSpan')
+        self.speakCellSpanCheckButton = \
+            Gtk.CheckButton.new_with_mnemonic(label)
+        self.speakCellSpanCheckButton.set_active(value)
+        tableGrid.attach(self.speakCellSpanCheckButton, 0, 1, 1, 1)
+
+        label = guilabels.TABLE_ANNOUNCE_CELL_HEADER
+        value = _settingsManager.getSetting('speakCellHeaders')
+        self.speakCellHeadersCheckButton = \
+            Gtk.CheckButton.new_with_mnemonic(label)
+        self.speakCellHeadersCheckButton.set_active(value)
+        tableGrid.attach(self.speakCellHeadersCheckButton, 0, 2, 1, 1)
+           
+        label = guilabels.TABLE_SKIP_BLANK_CELLS
+        value = _settingsManager.getSetting('skipBlankCells')
+        self.skipBlankCellsCheckButton = \
+            Gtk.CheckButton.new_with_mnemonic(label)
+        self.skipBlankCellsCheckButton.set_active(value)
+        tableGrid.attach(self.skipBlankCellsCheckButton, 0, 3, 1, 1)
+
+        findFrame = Gtk.Frame()
+        grid.attach(findFrame, 0, 2, 1, 1)
+
+        label = Gtk.Label(label="<b>%s</b>" % guilabels.FIND_OPTIONS)
+        label.set_use_markup(True)
+        findFrame.set_label_widget(label)
+
+        findAlignment = Gtk.Alignment.new(0.5, 0.5, 1, 1)
+        findAlignment.set_padding(0, 0, 12, 0)
+        findFrame.add(findAlignment)
+        findGrid = Gtk.Grid()
+        findAlignment.add(findGrid)
+
+        verbosity = _settingsManager.getSetting('findResultsVerbosity')
+
+        label = guilabels.FIND_SPEAK_RESULTS
+        value = verbosity != settings.FIND_SPEAK_NONE
+        self.speakResultsDuringFindCheckButton = \
+            Gtk.CheckButton.new_with_mnemonic(label)
+        self.speakResultsDuringFindCheckButton.set_active(value)
+        findGrid.attach(self.speakResultsDuringFindCheckButton, 0, 0, 1, 1)
+
+        label = guilabels.FIND_ONLY_SPEAK_CHANGED_LINES
+        value = verbosity == settings.FIND_SPEAK_IF_LINE_CHANGED
+        self.changedLinesOnlyCheckButton = \
+            Gtk.CheckButton.new_with_mnemonic(label)
+        self.changedLinesOnlyCheckButton.set_active(value)
+        findGrid.attach(self.changedLinesOnlyCheckButton, 0, 1, 1, 1)
+
+        hgrid = Gtk.Grid()
+        findGrid.attach(hgrid, 0, 2, 1, 1)
+
+        self.minimumFindLengthLabel = \
+              Gtk.Label(label=guilabels.FIND_MINIMUM_MATCH_LENGTH)
+        self.minimumFindLengthLabel.set_alignment(0, 0.5)
+        hgrid.attach(self.minimumFindLengthLabel, 0, 0, 1, 1)
+
+        self.minimumFindLengthAdjustment = \
+                   Gtk.Adjustment(_settingsManager.getSetting('findResultsMinimumLength'), 0, 20, 1)
+        self.minimumFindLengthSpinButton = Gtk.SpinButton()
+        self.minimumFindLengthSpinButton.set_adjustment(
+            self.minimumFindLengthAdjustment)
+        hgrid.attach(self.minimumFindLengthSpinButton, 1, 0, 1, 1)
+        self.minimumFindLengthLabel.set_mnemonic_widget(
+            self.minimumFindLengthSpinButton)
+
+        grid.show_all()
+
+        return grid
+
+    def getPreferencesFromGUI(self):
+        """Returns a dictionary with the app-specific preferences."""
+
+        if not self.speakResultsDuringFindCheckButton.get_active():
+            verbosity = settings.FIND_SPEAK_NONE
+        elif self.changedLinesOnlyCheckButton.get_active():
+            verbosity = settings.FIND_SPEAK_IF_LINE_CHANGED
+        else:
+            verbosity = settings.FIND_SPEAK_ALL
+
+        return {
+            'findResultsVerbosity': verbosity,
+            'findResultsMinimumLength': self.minimumFindLengthSpinButton.get_value(),
+            'sayAllOnLoad': self.sayAllOnLoadCheckButton.get_active(),
+            'structuralNavigationEnabled': self.structuralNavigationCheckButton.get_active(),
+            'structNavTriggersFocusMode': self.autoFocusModeStructNavCheckButton.get_active(),
+            'caretNavigationEnabled': self.controlCaretNavigationCheckButton.get_active(),
+            'caretNavTriggersFocusMode': self.autoFocusModeCaretNavCheckButton.get_active(),
+            'speakCellCoordinates': self.speakCellCoordinatesCheckButton.get_active(),
+            'layoutMode': self.layoutModeCheckButton.get_active(),
+            'speakCellSpan': self.speakCellSpanCheckButton.get_active(),
+            'speakCellHeaders': self.speakCellHeadersCheckButton.get_active(),
+            'skipBlankCells': self.skipBlankCellsCheckButton.get_active()
+        }
+
+    def consumesKeyboardEvent(self, keyboardEvent):
+        """Returns True if the script will consume this keyboard event."""
+
+        # We need to do this here. Orca caret and structural navigation
+        # often result in the user being repositioned without our getting
+        # a corresponding AT-SPI event. Without an AT-SPI event, script.py
+        # won't know to dump the generator cache. See bgo#618827.
+        self.generatorCache = {}
+
+        handler = self.keyBindings.getInputHandler(keyboardEvent)
+        if handler and self.caretNavigation.handles_navigation(handler):
+            consumes = self.useCaretNavigationModel(keyboardEvent)
+            self._lastCommandWasCaretNav = consumes
+            self._lastCommandWasStructNav = False
+            self._lastCommandWasMouseButton = False
+            return consumes
+
+        if handler and handler.function in self.structuralNavigation.functions:
+            consumes = self.useStructuralNavigationModel()
+            self._lastCommandWasCaretNav = False
+            self._lastCommandWasStructNav = consumes
+            self._lastCommandWasMouseButton = False
+            return consumes
+
+        if handler and handler.function in self.liveRegionManager.functions:
+            # This is temporary.
+            consumes = self.useStructuralNavigationModel()
+            self._lastCommandWasCaretNav = False
+            self._lastCommandWasStructNav = consumes
+            self._lastCommandWasMouseButton = False
+            return consumes
+
+        self._lastCommandWasCaretNav = False
+        self._lastCommandWasStructNav = False
+        self._lastCommandWasMouseButton = False
+        return handler != None
+
+    # TODO - JD: This needs to be moved out of the scripts.
+    def textLines(self, obj, offset=None):
+        """Creates a generator that can be used to iterate document content."""
+
+        if not self.utilities.inDocumentContent():
+            super().textLines(obj, offset)
             return
 
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.locusOfFocusChanged(self, event, oldFocus, newFocus)
+        self._sayAllIsInterrupted = False
+
+        sayAllStyle = _settingsManager.getSetting('sayAllStyle')
+        sayAllBySentence = sayAllStyle == settings.SAYALL_STYLE_SENTENCE
+        if offset == None:
+            obj, characterOffset = self.utilities.getCaretContext()
+        else:
+            characterOffset = offset
+
+        self._inSayAll = True
+        done = False
+        while not done:
+            if sayAllBySentence:
+                contents = self.utilities.getSentenceContentsAtOffset(obj, characterOffset)
+            else:
+                contents = self.utilities.getLineContentsAtOffset(obj, characterOffset)
+            self._sayAllContents = contents
+            for content in contents:
+                obj, startOffset, endOffset, text = content
+                utterances = self.speechGenerator.generateContents([content], eliminatePauses=True)
+
+                # TODO - JD: This is sad, but it's better than the old, broken
+                # clumpUtterances(). We really need to fix the speechservers'
+                # SayAll support. In the meantime, the generators should be
+                # providing one ACSS per string.
+                elements = list(filter(lambda x: isinstance(x, str), utterances[0]))
+                voices = list(filter(lambda x: isinstance(x, ACSS), utterances[0]))
+                if len(elements) != len(voices):
+                    continue
+
+                for i, element in enumerate(elements):
+                    context = speechserver.SayAllContext(
+                        obj, element, startOffset, endOffset)
+                    self._sayAllContexts.append(context)
+                    yield [context, voices[i]]
+
+            lastObj, lastOffset = contents[-1][0], contents[-1][2]
+            obj, characterOffset = self.utilities.findNextCaretInOrder(lastObj, lastOffset - 1)
+            if (obj, characterOffset) == (lastObj, lastOffset):
+                obj, characterOffset = self.utilities.findNextCaretInOrder(lastObj, lastOffset)
+
+            done = (obj == None)
+
+        self._inSayAll = False
+        self._sayAllContents = []
+        self._sayAllContexts = []
+
+    def presentFindResults(self, obj, offset):
+        """Updates the context and presents the find results if appropriate."""
+
+        text = self.utilities.queryNonEmptyText(obj)
+        if not (text and text.getNSelections()):
+            return
+
+        context = self.utilities.getCaretContext(documentFrame=None)
+
+        start, end = text.getSelection(0)
+        offset = max(offset, start)
+        self.utilities.setCaretContext(obj, offset, documentFrame=None)
+        if end - start < _settingsManager.getSetting('findResultsMinimumLength'):
+            return
+
+        verbosity = _settingsManager.getSetting('findResultsVerbosity')
+        if verbosity == settings.FIND_SPEAK_NONE:
+            return
+
+        if self._madeFindAnnouncement \
+           and verbosity == settings.FIND_SPEAK_IF_LINE_CHANGED \
+           and not self.utilities.contextsAreOnSameLine(context, (obj, offset)):
+            return
+
+        contents = self.utilities.getLineContentsAtOffset(obj, offset)
+        self.speakContents(contents)
+        self.updateBraille(obj)
+        self._madeFindAnnouncement = True
+
+    def sayAll(self, inputEvent, obj=None, offset=None):
+        """Speaks the contents of the document beginning with the present
+        location.  Overridden in this script because the sayAll could have
+        been started on an object without text (such as an image).
+        """
+
+        if not self.utilities.inDocumentContent():
+            return default.Script.sayAll(self, inputEvent, obj, offset)
+
+        else:
+            obj = obj or orca_state.locusOfFocus
+            speech.sayAll(self.textLines(obj, offset),
+                          self.__sayAllProgressCallback)
+
+        return True
+
+    def _rewindSayAll(self, context, minCharCount=10):
+        if not self.utilities.inDocumentContent():
+            return default.Script._rewindSayAll(self, context, minCharCount)
+
+        if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'):
+            return False
+
+        obj, start, end, string = self._sayAllContents[0]
+        orca.setLocusOfFocus(None, obj, notifyScript=False)
+        self.utilities.setCaretContext(obj, start)
+
+        prevObj, prevOffset = self.utilities.findPreviousCaretInOrder(obj, start)
+        self.sayAll(None, prevObj, prevOffset)
+        return True
+
+    def _fastForwardSayAll(self, context):
+        if not self.utilities.inDocumentContent():
+            return default.Script._fastForwardSayAll(self, context)
+
+        if not _settingsManager.getSetting('rewindAndFastForwardInSayAll'):
+            return False
+
+        obj, start, end, string = self._sayAllContents[-1]
+        orca.setLocusOfFocus(None, obj, notifyScript=False)
+        self.utilities.setCaretContext(obj, end)
+
+        nextObj, nextOffset = self.utilities.findNextCaretInOrder(obj, end)
+        self.sayAll(None, nextObj, nextOffset)
+        return True
+
+    def __sayAllProgressCallback(self, context, progressType):
+        if not self.utilities.inDocumentContent() or self._inFocusMode:
+            default.Script.__sayAllProgressCallback(self, context, progressType)
+            return
+
+        if progressType == speechserver.SayAllContext.INTERRUPTED:
+            if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
+                self._sayAllIsInterrupted = True
+                lastKey = orca_state.lastInputEvent.event_string
+                if lastKey == "Down" and self._fastForwardSayAll(context):
+                    return
+                elif lastKey == "Up" and self._rewindSayAll(context):
+                    return
+                elif not self._lastCommandWasStructNav:
+                    self.utilities.setCaretPosition(context.obj, context.currentOffset)
+                    self.updateBraille(context.obj)
+
+            self._inSayAll = False
+            self._sayAllContents = []
+            self._sayAllContexts = []
+            return
+
+        orca.setLocusOfFocus(None, context.obj, notifyScript=False)
+        self.utilities.setCaretContext(context.obj, context.currentOffset)
+
+    def _getCtrlShiftSelectionsStrings(self):
+        return [messages.LINE_SELECTED_DOWN,
+                messages.LINE_UNSELECTED_DOWN,
+                messages.LINE_SELECTED_UP,
+                messages.LINE_UNSELECTED_UP]
 
     def onActiveChanged(self, event):
         """Callback for object:state-changed:active accessibility events."""
@@ -109,72 +759,305 @@ class Script(web.Script):
     def onBusyChanged(self, event):
         """Callback for object:state-changed:busy accessibility events."""
 
-        if super().onBusyChanged(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onBusyChanged(self, event)
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onBusyChanged(event)
+            return False
+
+        if not self.utilities.inDocumentContent(orca_state.locusOfFocus):
+            msg = "INFO: Ignoring: Locus of focus is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        self._loadingDocumentContent = event.detail1
+
+        obj, offset = self.utilities.getCaretContext()
+        if not obj or self.utilities.isZombie(obj):
+            self.utilities.clearCaretContext()
+
+        if not _settingsManager.getSetting('onlySpeakDisplayedText'):
+            if event.detail1:
+                msg = messages.PAGE_LOADING_START
+            elif event.source.name:
+                msg = messages.PAGE_LOADING_END_NAMED % event.source.name
+            else:
+                msg = messages.PAGE_LOADING_END
+            self.presentMessage(msg)
+
+        if event.detail1:
+            return True
+
+        if self.useFocusMode(orca_state.locusOfFocus) != self._inFocusMode:
+            self.togglePresentationMode(None)
+
+        obj, offset = self.utilities.getCaretContext()
+        if not obj:
+            msg = "INFO: Could not get caret context"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.isFocusModeWidget(obj):
+            msg = "INFO: Setting locus of focus to focusModeWidget %s" % obj
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, obj)
+            return True
+
+        state = obj.getState()
+        if self.utilities.isLink(obj) and state.contains(pyatspi.STATE_FOCUSED):
+            msg = "INFO: Setting locus of focus to focused link %s" % obj
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, obj)
+            return True
+
+        if offset > 0:
+            msg = "INFO: Setting locus of focus to context obj %s" % obj
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, obj)
+            return True
+
+        self.updateBraille(obj)
+        if state.contains(pyatspi.STATE_FOCUSABLE):
+            msg = "INFO: Not doing SayAll due to focusable context obj %s" % obj
+            debug.println(debug.LEVEL_INFO, msg)
+            speech.speak(self.speechGenerator.generateSpeech(obj))
+        elif not _settingsManager.getSetting('sayAllOnLoad'):
+            msg = "INFO: Not doing SayAll due to sayAllOnLoad being False"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.speakContents(self.getLineContentsAtOffset(obj, offset))
+        elif _settingsManager.getSetting('enableSpeech'):
+            msg = "INFO: Doing SayAll"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.sayAll(None)
+        else:
+            msg = "INFO: Not doing SayAll due to enableSpeech being False"
+            debug.println(debug.LEVEL_INFO, msg)
+
+        return True
 
     def onCaretMoved(self, event):
         """Callback for object:text-caret-moved accessibility events."""
 
-        if super().onCaretMoved(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
+        if self.utilities.isZombie(event.source):
+            msg = "ERROR: Event source is Zombie"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onCaretMoved(event)
+            return False
+
+        if self._lastCommandWasCaretNav:
+            msg = "INFO: Event ignored: Last command was caret nav"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self._lastCommandWasStructNav:
+            msg = "INFO: Event ignored: Last command was struct nav"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self._lastCommandWasMouseButton:
+            msg = "INFO: Event handled: Last command was mouse button"
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, event.source)
+            self.utilities.setCaretContext(event.source, event.detail1)
+            return True
+
+        if self.utilities.inFindToolbar() and not self._madeFindAnnouncement:
+            msg = "INFO: Event handled: Presenting find results"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.presentFindResults(event.source, event.detail1)
+            return True
+
+        if self.utilities.eventIsAutocompleteNoise(event):
+            msg = "INFO: Event ignored: Autocomplete noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.textEventIsForNonNavigableTextObject(event):
+            msg = "INFO: Event ignored: Event source is non-navigable text object"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.textEventIsDueToInsertion(event):
+            msg = "INFO: Event handled: Updating position due to insertion"
+            debug.println(debug.LEVEL_INFO, msg)
+            self._saveLastCursorPosition(event.source, event.detail1)
+            return True
+
+        obj, offset = self.utilities.findFirstCaretContext(event.source, event.detail1)
+
+        if self.utilities.caretMovedToSamePageFragment(event):
+            msg = "INFO: Event handled: Caret moved to fragment"
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, obj)
+            self.utilities.setCaretContext(obj, offset)
+            return True
+
+        text = self.utilities.queryNonEmptyText(event.source)
+        if not text:
+            if event.source.getRole() == pyatspi.ROLE_LINK:
+                msg = "INFO: Event handled: Was for non-text link"
+                debug.println(debug.LEVEL_INFO, msg)
+                orca.setLocusOfFocus(event, event.source)
+                self.utilities.setCaretContext(event.source, event.detail1)
+            else:
+                msg = "INFO: Event ignored: Was for non-text non-link"
+                debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        char = text.getText(event.detail1, event.detail1+1)
+        isEditable = obj.getState().contains(pyatspi.STATE_EDITABLE)
+        if not char and not isEditable:
+            msg = "INFO: Event ignored: Was for empty char in non-editable text"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if char == self.EMBEDDED_OBJECT_CHARACTER:
+            if not self.utilities.isTextBlockElement(obj):
+                msg = "INFO: Event ignored: Was for embedded non-textblock"
+                debug.println(debug.LEVEL_INFO, msg)
+                return True
+
+            msg = "INFO: Setting locusOfFocus, context to: %s, %i" % (obj, offset)
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, obj)
+            self.utilities.setCaretContext(obj, offset)
+            return True
+
+        if not _settingsManager.getSetting('caretNavigationEnabled') \
+           or self._inFocusMode or isEditable:
+            orca.setLocusOfFocus(event, event.source, False)
+            self.utilities.setCaretContext(event.source, event.detail1)
+            msg = "INFO: Setting locusOfFocus, context to: %s, %i" % \
+                  (event.source, event.detail1)
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onCaretMoved(event)
+            return False
+
+        self.utilities.setCaretContext(obj, offset)
+        msg = "INFO: Setting context to: %s, %i" % (obj, offset)
         debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onCaretMoved(self, event)
+        super().onCaretMoved(event)
+        return False
 
     def onCheckedChanged(self, event):
         """Callback for object:state-changed:checked accessibility events."""
 
-        if super().onCheckedChanged(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onCheckedChanged(self, event)
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onCheckedChanged(event)
+            return False
+
+        obj, offset = self.utilities.getCaretContext()
+        if obj != event.source:
+            msg = "INFO: Event source is not context object"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        oldObj, oldState = self.pointOfReference.get('checkedChange', (None, 0))
+        if hash(oldObj) == hash(obj) and oldState == event.detail1:
+            msg = "INFO: Ignoring event, state hasn't changed"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        role = obj.getRole()
+        if not (self._lastCommandWasCaretNav and role == pyatspi.ROLE_RADIO_BUTTON):
+            msg = "INFO: Event is something default can handle"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onCheckedChanged(event)
+            return False
+
+        self.updateBraille(obj)
+        speech.speak(self.speechGenerator.generateSpeech(obj, alreadyFocused=True))
+        self.pointOfReference['checkedChange'] = hash(obj), event.detail1
+        return True
 
     def onChildrenChanged(self, event):
         """Callback for object:children-changed accessibility events."""
 
-        if super().onChildrenChanged(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onChildrenChanged(self, event)
+        if self.utilities.handleAsLiveRegion(event):
+            msg = "INFO: Event to be handled as live region"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.liveRegionManager.handleEvent(event)
+            return True
+
+        if self._loadingDocumentContent:
+            msg = "INFO: Ignoring because document content is being loaded."
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not event.any_data or self.utilities.isZombie(event.any_data):
+            msg = "INFO: Ignoring because any data is null or zombified."
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onChildrenChanged(event)
+            return False
+
+        obj, offset = self.utilities.getCaretContext()
+        if obj and self.utilities.isZombie(obj):
+            replicant = self.utilities.findReplicant(event.source, obj)
+            if replicant:
+                # Refrain from actually touching the replicant by grabbing
+                # focus or setting the caret in it. Doing so will only serve
+                # to anger it.
+                msg = "INFO: Event handled by updating locusOfFocus and context"
+                debug.println(debug.LEVEL_INFO, msg)
+                orca.setLocusOfFocus(event, replicant, False)
+                self.utilities.setCaretContext(replicant, offset)
+                return True
+
+        child = event.any_data
+        if child.getRole() in [pyatspi.ROLE_ALERT, pyatspi.ROLE_DIALOG]:
+            msg = "INFO: Setting locusOfFocus to event.any_data"
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, child)
+            return True
+
+        if self.lastMouseRoutingTime and 0 < time.time() - self.lastMouseRoutingTime < 1:
+            utterances = []
+            utterances.append(messages.NEW_ITEM_ADDED)
+            utterances.extend(self.speechGenerator.generateSpeech(child, force=True))
+            speech.speak(utterances)
+            self.lastMouseOverObject = child
+            self.preMouseOverContext = self.utilities.getCaretContext()
+            return True
+
+        super().onChildrenChanged(event)
+        return False
 
     def onDocumentLoadComplete(self, event):
         """Callback for document:load-complete accessibility events."""
 
-        if super().onDocumentLoadComplete(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
+        msg = "INFO: Updating loading state and resetting live regions"
         debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onDocumentLoadComplete(self, event)
+        self._loadingDocumentContent = False
+        self.liveRegionManager.reset()
+        return True
 
     def onDocumentLoadStopped(self, event):
         """Callback for document:load-stopped accessibility events."""
 
-        if super().onDocumentLoadStopped(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
+        msg = "INFO: Updating loading state"
         debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onDocumentLoadStopped(self, event)
+        self._loadingDocumentContent = False
+        return True
 
     def onDocumentReload(self, event):
         """Callback for document:reload accessibility events."""
 
-        if super().onDocumentReload(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
+        msg = "INFO: Updating loading state"
         debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onDocumentReload(self, event)
+        self._loadingDocumentContent = True
+        return True
 
     def onFocus(self, event):
         """Callback for focus: accessibility events."""
@@ -185,7 +1068,7 @@ class Script(web.Script):
 
         # NOTE: This event type is deprecated and Orca should no longer use it.
         # This callback remains just to handle bugs in applications and toolkits
-        # in which object:state-changed:focused events are missing.
+        # during the remainder of the unstable (3.11) development cycle.
 
         role = event.source.getRole()
 
@@ -213,72 +1096,259 @@ class Script(web.Script):
     def onFocusedChanged(self, event):
         """Callback for object:state-changed:focused accessibility events."""
 
-        if super().onFocusedChanged(event):
-            return
+        if not event.detail1:
+            msg = "INFO: Ignoring because event source lost focus"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.isZombie(event.source):
+            msg = "ERROR: Event source is Zombie"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onFocusedChanged(event)
+            return False
+
+        state = event.source.getState()
+        if state.contains(pyatspi.STATE_EDITABLE):
+            msg = "INFO: Event source is editable"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onFocusedChanged(event)
+            return False
 
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onFocusedChanged(self, event)
+        role = event.source.getRole()
+        if role in [pyatspi.ROLE_DIALOG, pyatspi.ROLE_ALERT]:
+            msg = "INFO: Event handled: Setting locusOfFocus to event source"
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, event.source)
+            return True
+
+        if self._lastCommandWasCaretNav:
+            msg = "INFO: Event ignored: Last command was caret nav"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self._lastCommandWasStructNav:
+            msg = "INFO: Event ignored: Last command was struct nav"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if role in [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_DOCUMENT_WEB]:
+            obj, offset = self.utilities.getCaretContext(event.source)
+            if obj and self.utilities.isZombie(obj):
+                msg = "INFO: Clearing context - obj is zombie"
+                debug.println(debug.LEVEL_INFO, msg)
+                self.utilities.clearCaretContext()
+                obj, offset = self.utilities.getCaretContext(event.source)
+
+            if obj:
+                wasFocused = obj.getState().contains(pyatspi.STATE_FOCUSED)
+                obj.clearCache()
+                isFocused = obj.getState().contains(pyatspi.STATE_FOCUSED)
+                if wasFocused == isFocused:
+                    msg = "INFO: Event handled: Setting locusOfFocus to context"
+                    debug.println(debug.LEVEL_INFO, msg)
+                    orca.setLocusOfFocus(event, obj)
+                    return True
+
+        if not state.contains(pyatspi.STATE_FOCUSABLE) \
+           and not state.contains(pyatspi.STATE_FOCUSED):
+            msg = "INFO: Event ignored: Source is not focusable or focused"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        super().onFocusedChanged(event)
+        return False
 
     def onMouseButton(self, event):
         """Callback for mouse:button accessibility events."""
 
-        if super().onMouseButton(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onMouseButton(self, event)
+        self._lastCommandWasCaretNav = False
+        self._lastCommandWasStructNav = False
+        self._lastCommandWasMouseButton = True
+        super().onMouseButton(event)
+        return False
 
     def onNameChanged(self, event):
         """Callback for object:property-change:accessible-name events."""
 
-        if super().onNameChanged(event):
-            return
+        if self.utilities.eventIsStatusBarNoise(event):
+            msg = "INFO: Ignoring event believed to be status bar noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
 
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onNameChanged(self, event)
+        if event.source.getRole() == pyatspi.ROLE_FRAME:
+            msg = "INFO: Flusing messages from live region manager"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.liveRegionManager.flushMessages()
+
+        return True
 
     def onShowingChanged(self, event):
         """Callback for object:state-changed:showing accessibility events."""
 
-        if super().onShowingChanged(event):
-            return
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onShowingChanged(event)
+            return False
 
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onShowingChanged(self, event)
+        return True
 
     def onTextDeleted(self, event):
         """Callback for object:text-changed:delete accessibility events."""
 
-        if super().onTextDeleted(event):
-            return
+        if self.utilities.eventIsStatusBarNoise(event):
+            msg = "INFO: Ignoring event believed to be status bar noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onTextDeleted(event)
+            return False
+
+        if self.utilities.eventIsAutocompleteNoise(event):
+            msg = "INFO: Ignoring event believed to be autocomplete noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
 
-        msg = "GECKO: Passing along event to default script"
+        if self.utilities.textEventIsDueToInsertion(event):
+            msg = "INFO: Ignoring event believed to be due to text insertion"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        msg = "INFO: Clearing content cache due to text deletion"
         debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onTextDeleted(self, event)
+        self.utilities.clearContentCache()
+
+        state = event.source.getState()
+        if not state.contains(pyatspi.STATE_EDITABLE):
+            if self.inMouseOverObject \
+               and self.utilities.isZombie(self.lastMouseOverObject):
+                msg = "INFO: Restoring pre-mouseover context"
+                debug.println(debug.LEVEL_INFO, msg)
+                self.restorePreMouseOverContext()
+
+            msg = "INFO: Done processing non-editable source"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        super().onTextDeleted(event)
+        return False
 
     def onTextInserted(self, event):
         """Callback for object:text-changed:insert accessibility events."""
 
-        if super().onTextInserted(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
+        if self.utilities.eventIsStatusBarNoise(event):
+            msg = "INFO: Ignoring event believed to be status bar noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onTextInserted(event)
+            return False
+
+        if self.utilities.eventIsAutocompleteNoise(event):
+            msg = "INFO: Ignoring: Event believed to be autocomplete noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        # TODO - JD: As an experiment, we're stopping these at the event manager.
+        # If that works, this can be removed.
+        if self.utilities.eventIsEOCAdded(event):
+            msg = "INFO: Ignoring: Event was for embedded object char"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        msg = "INFO: Clearing content cache due to text insertion"
         debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onTextInserted(self, event)
+        self.utilities.clearContentCache()
+
+        if self.utilities.handleAsLiveRegion(event):
+            msg = "INFO: Event to be handled as live region"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.liveRegionManager.handleEvent(event)
+            return True
+
+        text = self.utilities.queryNonEmptyText(event.source)
+        if not text:
+            msg = "INFO: Ignoring: Event source is not a text object"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        state = event.source.getState()
+        if not state.contains(pyatspi.STATE_EDITABLE) \
+           and event.source != orca_state.locusOfFocus:
+            msg = "INFO: Done processing non-editable, non-locusOfFocus source"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        super().onTextInserted(event)
+        return False
 
     def onTextSelectionChanged(self, event):
         """Callback for object:text-selection-changed accessibility events."""
 
-        if super().onTextSelectionChanged(event):
-            return
-
-        msg = "GECKO: Passing along event to default script"
-        debug.println(debug.LEVEL_INFO, msg)
-        default.Script.onTextSelectionChanged(self, event)
+        if self.utilities.isZombie(event.source):
+            msg = "ERROR: Event source is Zombie"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "INFO: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            super().onTextSelectionChanged(event)
+            return False
+
+        if self.utilities.inFindToolbar():
+            msg = "INFO: Event handled: Presenting find results"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.presentFindResults(event.source, -1)
+            self._saveFocusedObjectInfo(orca_state.locusOfFocus)
+            return True
+
+        if not self.utilities.inDocumentContent(orca_state.locusOfFocus):
+            msg = "INFO: Ignoring: Event in document content; focus is not"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.eventIsAutocompleteNoise(event):
+            msg = "INFO: Ignoring: Event believed to be autocomplete noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.textEventIsForNonNavigableTextObject(event):
+            msg = "INFO: Ignoring event for non-navigable text object"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        text = self.utilities.queryNonEmptyText(event.source)
+        if not text:
+            msg = "INFO: Ignoring: Event source is not a text object"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        char = text.getText(event.detail1, event.detail1+1)
+        if char == self.EMBEDDED_OBJECT_CHARACTER:
+            msg = "INFO: Ignoring: Event offset is at embedded object"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        obj, offset = self.utilities.getCaretContext()
+        if obj and obj.parent and event.source in [obj.parent, obj.parent.parent]:
+            msg = "INFO: Ignoring: Source is context ancestor"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        super().onTextSelectionChanged(event)
+        return False
 
     def handleProgressBarUpdate(self, event, obj):
         """Determine whether this progress bar event should be spoken or not.
@@ -297,3 +1367,381 @@ class Script(web.Script):
                      pyatspi.ROLE_APPLICATION]
         if not self.utilities.hasMatchingHierarchy(event.source, rolesList):
             default.Script.handleProgressBarUpdate(self, event, obj)
+
+    def inFocusMode(self):
+        """ Returns True if we're in focus mode."""
+
+        return self._inFocusMode
+
+    def focusModeIsSticky(self):
+        """Returns True if we're in 'sticky' focus mode."""
+
+        return self._focusModeIsSticky
+
+    def useFocusMode(self, obj):
+        """Returns True if we should use focus mode in obj."""
+
+        if self._focusModeIsSticky:
+            return True
+
+        if not _settingsManager.getSetting('structNavTriggersFocusMode') \
+           and self._lastCommandWasStructNav:
+            return False
+
+        if not _settingsManager.getSetting('caretNavTriggersFocusMode') \
+           and self._lastCommandWasCaretNav:
+            return False
+
+        return self.utilities.isFocusModeWidget(obj)
+
+    def locusOfFocusChanged(self, event, oldFocus, newFocus):
+        """Handles changes of focus of interest to the script."""
+
+        if newFocus and self.utilities.isZombie(newFocus):
+            msg = "ERROR: New focus is Zombie" % newFocus
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(newFocus):
+            msg = "INFO: Locus of focus changed to non-document obj"
+            self._madeFindAnnouncement = False
+            self._inFocusMode = False
+            debug.println(debug.LEVEL_INFO, msg)
+            super().locusOfFocusChanged(event, oldFocus, newFocus)
+            return False
+
+        if oldFocus and self.utilities.isZombie(oldFocus):
+            oldFocus = None
+
+        caretOffset = 0
+        if self.utilities.inFindToolbar(oldFocus):
+            newFocus, caretOffset = self.utilities.getCaretContext()
+
+        text = self.utilities.queryNonEmptyText(newFocus)
+        if text and (0 <= text.caretOffset < text.characterCount):
+            caretOffset = text.caretOffset
+
+        self.utilities.setCaretContext(newFocus, caretOffset)
+        self.updateBraille(newFocus)
+        speech.speak(self.speechGenerator.generateSpeech(newFocus, priorObj=oldFocus))
+        self._saveFocusedObjectInfo(newFocus)
+
+        if not self._focusModeIsSticky \
+           and self.useFocusMode(newFocus) != self._inFocusMode:
+            self.togglePresentationMode(None)
+
+        return True
+
+    def updateBraille(self, obj, extraRegion=None):
+        """Updates the braille display to show the given object."""
+
+        if not _settingsManager.getSetting('enableBraille') \
+           and not _settingsManager.getSetting('enableBrailleMonitor'):
+            debug.println(debug.LEVEL_INFO, "BRAILLE: disabled")
+            return
+
+        if not (self._lastCommandWasCaretNav or self._lastCommandWasStructNav) \
+           or self._inFocusMode or not self.utilities.inDocumentContent():
+            super().updateBraille(obj, extraRegion)
+            return
+
+        obj, offset = self.utilities.getCaretContext(documentFrame=None)
+        contents = self.utilities.getLineContentsAtOffset(obj, offset)
+        self.displayContents(contents)
+
+    def displayContents(self, contents):
+        """Displays contents in braille."""
+
+        if not _settingsManager.getSetting('enableBraille') \
+           and not _settingsManager.getSetting('enableBrailleMonitor'):
+            debug.println(debug.LEVEL_INFO, "BRAILLE: disabled")
+            return
+
+        line = self.getNewBrailleLine(clearBraille=True, addLine=True)
+        regions, focusedRegion = self.brailleGenerator.generateContents(contents)
+        for region in regions:
+            self.addBrailleRegionsToLine(region, line)
+
+        if line.regions:
+            line.regions[-1].string = line.regions[-1].string.rstrip(" ")
+
+        self.setBrailleFocus(focusedRegion, getLinkMask=False)
+        self.refreshBraille(panToCursor=True, getLinkMask=False)
+
+    def speakContents(self, contents):
+        """Speaks the specified contents."""
+
+        utterances = self.speechGenerator.generateContents(contents)
+        speech.speak(utterances)
+
+    def speakCharacterAtOffset(self, obj, characterOffset):
+        """Speaks the character at the given characterOffset in the
+        given object."""
+        character = self.utilities.getCharacterAtOffset(obj, characterOffset)
+        self.speakMisspelledIndicator(obj, characterOffset)
+        if obj:
+            if character and character != self.EMBEDDED_OBJECT_CHARACTER:
+                self.speakCharacter(character)
+            elif not obj.getState().contains(pyatspi.STATE_EDITABLE):
+                # We won't have a character if we move to the end of an
+                # entry (in which case we're not on a character and therefore
+                # have nothing to say), or when we hit a component with no
+                # text (e.g. checkboxes) or reset the caret to the parent's
+                # characterOffset (lists).  In these latter cases, we'll just
+                # speak the entire component.
+                #
+                utterances = self.speechGenerator.generateSpeech(obj)
+                speech.speak(utterances)
+
+    def sayCharacter(self, obj):
+        """Speaks the character at the current caret position."""
+
+        if not self._lastCommandWasCaretNav:
+            super().sayCharacter(obj)
+            return
+
+        obj, offset = self.utilities.getCaretContext(documentFrame=None)
+        if not obj:
+            return
+
+        contents = self.utilities.getCharacterContentsAtOffset(obj, offset)
+        if not contents:
+            return
+
+        obj, start, end, string = contents[0]
+        if start > 0:
+            string = string or "\n"
+
+        if string:
+            self.speakMisspelledIndicator(obj, start)
+            self.speakCharacter(string)
+        else:
+            self.speakContents(contents)
+
+    def sayWord(self, obj):
+        """Speaks the word at the current caret position."""
+
+        if not self._lastCommandWasCaretNav:
+            super().sayWord(obj)
+            return
+
+        obj, offset = self.utilities.getCaretContext(documentFrame=None)
+        wordContents = self.utilities.getWordContentsAtOffset(obj, offset)
+        textObj, startOffset, endOffset, word = wordContents[0]
+        self.speakMisspelledIndicator(textObj, startOffset)
+        self.speakContents(wordContents)
+
+    def sayLine(self, obj):
+        """Speaks the line at the current caret position."""
+
+        if not (self._lastCommandWasCaretNav or self._lastCommandWasStructNav):
+            super().sayLine(obj)
+            return
+
+        obj, offset = self.utilities.getCaretContext(documentFrame=None)
+        self.speakContents(self.utilities.getLineContentsAtOffset(obj, offset))
+
+    def presentObject(self, obj, offset=0):
+        contents = self.utilities.getObjectContentsAtOffset(obj, offset)
+        self.displayContents(contents)
+        self.speakContents(contents)
+
+    def panBrailleLeft(self, inputEvent=None, panAmount=0):
+        """In document content, we want to use the panning keys to browse the
+        entire document.
+        """
+        if self.flatReviewContext \
+           or not self.utilities.inDocumentContent() \
+           or not self.isBrailleBeginningShowing():
+            default.Script.panBrailleLeft(self, inputEvent, panAmount)
+        else:
+            self.goPreviousLine(inputEvent)
+            while self.panBrailleInDirection(panToLeft=False):
+                pass
+            self.refreshBraille(False)
+        return True
+
+    def panBrailleRight(self, inputEvent=None, panAmount=0):
+        """In document content, we want to use the panning keys to browse the
+        entire document.
+        """
+        if self.flatReviewContext \
+           or not self.utilities.inDocumentContent() \
+           or not self.isBrailleEndShowing():
+            default.Script.panBrailleRight(self, inputEvent, panAmount)
+        elif self.goNextLine(inputEvent):
+            while self.panBrailleInDirection(panToLeft=True):
+                pass
+            self.refreshBraille(False)
+        return True
+
+    def useCaretNavigationModel(self, keyboardEvent):
+        """Returns True if we should do our own caret navigation."""
+
+        if not _settingsManager.getSetting('caretNavigationEnabled') \
+           or self._inFocusMode:
+            return False
+
+        if not self.utilities.inDocumentContent():
+            return False
+
+        if keyboardEvent.modifiers & keybindings.SHIFT_MODIFIER_MASK:
+            return False
+
+        return True
+
+    def useStructuralNavigationModel(self):
+        """Returns True if we should do our own structural navigation.
+        This should return False if we're in something like an entry
+        or a list.
+        """
+
+        if not self.structuralNavigation.enabled or self._inFocusMode:
+            return False
+
+        if not self.utilities.inDocumentContent():
+            return False
+
+        return True
+ 
+    ####################################################################
+    #                                                                  #
+    # Methods to get information about current object.                 #
+    #                                                                  #
+    ####################################################################
+
+    def getTextLineAtCaret(self, obj, offset=None, startOffset=None, endOffset=None):
+        """To-be-removed. Returns the string, caretOffset, startOffset."""
+
+        if self._inFocusMode or not self.utilities.inDocumentContent(obj) \
+           or obj.getState().contains(pyatspi.STATE_EDITABLE):
+            return super().getTextLineAtCaret(obj, offset, startOffset, endOffset)
+
+        text = self.utilities.queryNonEmptyText(obj)
+        if offset is None:
+            try:
+                offset = max(0, text.caretOffset)
+            except:
+                offset = 0
+
+        if text and startOffset is not None and endOffset is not None:
+            return text.getText(startOffset, endOffset), offset, startOffset
+
+        contextObj, contextOffset = self.utilities.getCaretContext(documentFrame=None)
+        if contextObj == obj:
+            caretOffset = contextOffset
+        else:
+            caretOffset = offset
+
+        contents = self.utilities.getLineContentsAtOffset(obj, offset)
+        contents = list(filter(lambda x: x[0] == obj, contents))
+        if len(contents) == 1:
+            index = 0
+        else:
+            index = self.utilities.findObjectInContents(obj, offset, contents)
+
+        if index > -1:
+            candidate, startOffset, endOffset, string = contents[index]
+            if not self.EMBEDDED_OBJECT_CHARACTER in string:
+                return string, caretOffset, startOffset
+
+        return "", 0, 0
+
+    ####################################################################
+    #                                                                  #
+    # Methods to navigate to previous and next objects.                #
+    #                                                                  #
+    ####################################################################
+
+    def moveToMouseOver(self, inputEvent):
+        """Positions the caret offset to the next character or object
+        in the mouse over which has just appeared.
+        """
+
+        if not self.lastMouseOverObject:
+            self.presentMessage(messages.MOUSE_OVER_NOT_FOUND)
+            return
+
+        if not self.inMouseOverObject:
+            obj = self.lastMouseOverObject
+            offset = 0
+            if obj and not obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+                [obj, offset] = self.utilities.findFirstCaretContext(obj, offset)
+
+            if obj and obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+                obj.queryComponent().grabFocus()
+            elif obj:
+                contents = self.utilities.getObjectContentsAtOffset(obj, offset)
+                # If we don't have anything to say, let's try one more
+                # time.
+                #
+                if len(contents) == 1 and not contents[0][3].strip():
+                    [obj, offset] = self.utilities.findNextCaretInOrder(obj, offset)
+                    contents = self.utilities.getObjectContentsAtOffset(obj, offset)
+                self.utilities.setCaretPosition(obj, offset)
+                self.speakContents(contents)
+                self.updateBraille(obj)
+            self.inMouseOverObject = True
+        else:
+            # Route the mouse pointer where it was before both to "clean up
+            # after ourselves" and also to get the mouse over object to go
+            # away.
+            #
+            x, y = self.oldMouseCoordinates
+            eventsynthesizer.routeToPoint(x, y)
+            self.restorePreMouseOverContext()
+
+    def restorePreMouseOverContext(self):
+        """Cleans things up after a mouse-over object has been hidden."""
+
+        obj, offset = self.preMouseOverContext
+        if obj and not obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+            [obj, offset] = self.utilities.findFirstCaretContext(obj, offset)
+
+        if obj and obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+            obj.queryComponent().grabFocus()
+        elif obj:
+            self.utilities.setCaretPosition(obj, offset)
+            self.speakContents(self.utilities.getObjectContentsAtOffset(obj, offset))
+            self.updateBraille(obj)
+        self.inMouseOverObject = False
+        self.lastMouseOverObject = None
+
+    def enableStickyFocusMode(self, inputEvent):
+        self._inFocusMode = True
+        self._focusModeIsSticky = True
+        self.presentMessage(messages.MODE_FOCUS_IS_STICKY)
+
+    def togglePresentationMode(self, inputEvent):
+        if self._inFocusMode:
+            [obj, characterOffset] = self.utilities.getCaretContext()
+            try:
+                parentRole = obj.parent.getRole()
+            except:
+                parentRole = None
+            if parentRole == pyatspi.ROLE_LIST_BOX:
+                self.utilities.setCaretContext(obj.parent, -1)
+            elif parentRole == pyatspi.ROLE_MENU:
+                self.utilities.setCaretContext(obj.parent.parent, -1)
+
+            self.presentMessage(messages.MODE_BROWSE)
+        else:
+            self.presentMessage(messages.MODE_FOCUS)
+        self._inFocusMode = not self._inFocusMode
+        self._focusModeIsSticky = False
+
+    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.
+        """
+        if self.utilities.inDocumentContent(acc):
+            try:
+                ai = acc.queryAction()
+            except NotImplementedError:
+                return True
+        default.Script.speakWordUnderMouse(self, acc)
diff --git a/src/orca/scripts/toolkits/Gecko/script_utilities.py 
b/src/orca/scripts/toolkits/Gecko/script_utilities.py
index c5d5d8f..50a3124 100644
--- a/src/orca/scripts/toolkits/Gecko/script_utilities.py
+++ b/src/orca/scripts/toolkits/Gecko/script_utilities.py
@@ -31,17 +31,265 @@ __copyright__ = "Copyright (c) 2010 Joanmarie Diggs." \
 __license__   = "LGPL"
 
 import pyatspi
+import re
+import urllib
 
 from orca import debug
+from orca import input_event
+from orca import orca
 from orca import orca_state
-from orca.scripts import web
+from orca import script_utilities
+from orca import settings
+from orca import settings_manager
 
+_settingsManager = settings_manager.getManager()
 
-class Utilities(web.Utilities):
+
+class Utilities(script_utilities.Utilities):
 
     def __init__(self, script):
         super().__init__(script)
 
+        self._currentAttrs = {}
+        self._caretContexts = {}
+        self._inDocumentContent = {}
+        self._isTextBlockElement = {}
+        self._isGridDescendant = {}
+        self._isOffScreenLabel = {}
+        self._hasNoSize = {}
+        self._hasLongDesc = {}
+        self._isClickableElement = {}
+        self._isAnchor = {}
+        self._isLandmark = {}
+        self._isLiveRegion = {}
+        self._isLink = {}
+        self._isNonNavigablePopup = {}
+        self._isNonEntryTextWidget = {}
+        self._inferredLabels = {}
+        self._text = {}
+        self._currentObjectContents = None
+        self._currentSentenceContents = None
+        self._currentLineContents = None
+        self._currentWordContents = None
+        self._currentCharacterContents = None
+
+    def _cleanupContexts(self):
+        toRemove = []
+        for key, [obj, offset] in self._caretContexts.items():
+            if self.isZombie(obj):
+                toRemove.append(key)
+
+        for key in toRemove:
+            self._caretContexts.pop(key, None)
+
+    def clearCachedObjects(self):
+        debug.println(debug.LEVEL_INFO, "INFO: cleaning up cached objects")
+        self._inDocumentContent = {}
+        self._isTextBlockElement = {}
+        self._isGridDescendant = {}
+        self._isOffScreenLabel = {}
+        self._hasNoSize = {}
+        self._hasLongDesc = {}
+        self._isClickableElement = {}
+        self._isAnchor = {}
+        self._isLandmark = {}
+        self._isLiveRegion = {}
+        self._isLink = {}
+        self._isNonNavigablePopup = {}
+        self._isNonEntryTextWidget = {}
+        self._inferredLabels = {}
+        self._cleanupContexts()
+
+    def clearContentCache(self):
+        self._currentObjectContents = None
+        self._currentSentenceContents = None
+        self._currentLineContents = None
+        self._currentWordContents = None
+        self._currentCharacterContents = None
+        self._currentAttrs = {}
+        self._text = {}
+
+    def inDocumentContent(self, obj=None):
+        if not obj:
+            obj = orca_state.locusOfFocus
+
+        rv = self._inDocumentContent.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        document = self.getDocumentForObject(obj)
+        rv = document is not None
+        self._inDocumentContent[hash(obj)] = rv
+        return rv
+
+    def getDocumentForObject(self, obj):
+        if not obj:
+            return None
+
+        roles = [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_DOCUMENT_WEB, pyatspi.ROLE_EMBEDDED]
+        isDocument = lambda x: x and x.getRole() in roles
+        if isDocument(obj):
+            return obj
+
+        return pyatspi.findAncestor(obj, isDocument)
+
+    def _getDocumentsEmbeddedBy(self, frame):
+        isEmbeds = lambda r: r.getRelationType() == pyatspi.RELATION_EMBEDS
+        relations = list(filter(isEmbeds, frame.getRelationSet()))
+        if not relations:
+            return []
+
+        relation = relations[0]
+        targets = [relation.getTarget(i) for i in range(relation.getNTargets())]
+        if not targets:
+            return []
+
+        roles = [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_DOCUMENT_WEB]
+        isDocument = lambda x: x and x.getRole() in roles
+        return list(filter(isDocument, targets))
+
+    def documentFrame(self, obj=None):
+        isShowing = lambda x: x and x.getState().contains(pyatspi.STATE_SHOWING)
+
+        windows = [child for child in self._script.app]
+        if orca_state.activeWindow in windows:
+            windows = [orca_state.activeWindow]
+
+        for window in windows:
+            documents = self._getDocumentsEmbeddedBy(window)
+            documents = list(filter(isShowing, documents))
+            if len(documents) == 1:
+                return documents[0]
+
+        return self.getDocumentForObject(obj or orca_state.locusOfFocus)
+
+    def documentFrameURI(self):
+        documentFrame = self.documentFrame()
+        if documentFrame and not self.isZombie(documentFrame):
+            document = documentFrame.queryDocument()
+            return document.getAttributeValue('DocURL')
+
+        return None
+
+    def setCaretPosition(self, obj, offset):
+        if self._script.flatReviewContext:
+            self._script.toggleFlatReviewMode()
+
+        obj, offset = self.findFirstCaretContext(obj, offset)
+        self.setCaretContext(obj, offset, documentFrame=None)
+        if self._script.focusModeIsSticky():
+            return
+
+        try:
+            state = obj.getState()
+        except:
+            return
+
+        orca.setLocusOfFocus(None, obj, notifyScript=False)
+        if state.contains(pyatspi.STATE_FOCUSABLE):
+            try:
+                obj.queryComponent().grabFocus()
+            except:
+                return
+
+        text = self.queryNonEmptyText(obj)
+        if text:
+            text.setCaretOffset(offset)
+
+        if self._script.useFocusMode(obj) != self._script.inFocusMode():
+            self._script.togglePresentationMode(None)
+
+        obj.clearCache()
+
+        # TODO - JD: This is private.
+        self._script._saveFocusedObjectInfo(obj)
+
+    def getNextObjectInDocument(self, obj, documentFrame):
+        if not obj:
+            return None
+
+        for relation in obj.getRelationSet():
+            if relation.getRelationType() == pyatspi.RELATION_FLOWS_TO:
+                return relation.getTarget(0)
+
+        if obj == documentFrame:
+            obj, offset = self.getCaretContext(documentFrame)
+            for child in documentFrame:
+                if self.characterOffsetInParent(child) > offset:
+                    return child
+
+        if obj and obj.childCount:
+            return obj[0]
+
+        nextObj = None
+        while obj and not nextObj:
+            index = obj.getIndexInParent() + 1
+            if 0 < index < obj.parent.childCount:
+                nextObj = obj.parent[index]
+            elif obj.parent != documentFrame:
+                obj = obj.parent
+            else:
+                break
+
+        return nextObj
+
+    def getPreviousObjectInDocument(self, obj, documentFrame):
+        if not obj:
+            return None
+
+        for relation in obj.getRelationSet():
+            if relation.getRelationType() == pyatspi.RELATION_FLOWS_FROM:
+                return relation.getTarget(0)
+
+        if obj == documentFrame:
+            obj, offset = self.getCaretContext(documentFrame)
+            for child in documentFrame:
+                if self.characterOffsetInParent(child) < offset:
+                    return child
+
+        index = obj.getIndexInParent() - 1
+        if not 0 <= index < obj.parent.childCount:
+            obj = obj.parent
+            index = obj.getIndexInParent() - 1
+
+        previousObj = obj.parent[index]
+        while previousObj and previousObj.childCount:
+            previousObj = previousObj[previousObj.childCount - 1]
+
+        return previousObj
+
+    def getTopOfFile(self):
+        return self.findFirstCaretContext(self.documentFrame(), 0)
+
+    def getBottomOfFile(self):
+        obj = self.getLastObjectInDocument(self.documentFrame())
+        offset = 0
+        text = self.queryNonEmptyText(obj)
+        if text:
+            offset = text.characterCount - 1
+
+        while obj:
+            lastobj, lastoffset = self.nextContext(obj, offset)
+            if not lastobj:
+                break
+            obj, offset = lastobj, lastoffset
+
+        return [obj, offset]
+
+    def getLastObjectInDocument(self, documentFrame):
+        try:
+            lastChild = documentFrame[documentFrame.childCount - 1]
+        except:
+            lastChild = documentFrame
+        while lastChild:
+            lastObj = self.getNextObjectInDocument(lastChild, documentFrame)
+            if lastObj and lastObj != lastChild:
+                lastChild = lastObj
+            else:
+                break
+
+        return lastChild
+
     def inFindToolbar(self, obj=None):
         if not obj:
             obj = orca_state.locusOfFocus
@@ -52,8 +300,32 @@ class Utilities(web.Utilities):
 
         return super().inFindToolbar(obj)
 
+    def isHidden(self, obj):
+        try:
+            attrs = dict([attr.split(':', 1) for attr in obj.getAttributes()])
+        except:
+            return False
+        return attrs.get('hidden', False)
+
+    def isTextArea(self, obj):
+        if self.isLink(obj):
+            return False
+
+        return super().isTextArea(obj)
+
+    def isReadOnlyTextArea(self, obj):
+        # NOTE: This method is deliberately more conservative than isTextArea.
+        if obj.getRole() != pyatspi.ROLE_ENTRY:
+            return False
+
+        state = obj.getState()
+        readOnly = state.contains(pyatspi.STATE_FOCUSABLE) \
+                   and not state.contains(pyatspi.STATE_EDITABLE)
+
+        return readOnly
+
     def nodeLevel(self, obj):
-        """Determines the level of at which this object is at by using
+        """ Determines the level of at which this object is at by using
         the object attribute 'level'.  To be consistent with the default
         nodeLevel() this value is 0-based (Gecko return is 1-based) """
 
@@ -187,3 +459,1529 @@ class Utilities(web.Utilities):
                 break
 
         return descendants
+
+    def setCaretOffset(self, obj, characterOffset):
+        self.setCaretPosition(obj, characterOffset)
+        self._script.updateBraille(obj)
+
+    def nextContext(self, obj=None, offset=-1, skipSpace=False):
+        if not obj:
+            obj, offset = self.getCaretContext()
+
+        nextobj, nextoffset = self.findNextCaretInOrder(obj, offset)
+        if (obj, offset) == (nextobj, nextoffset):
+            nextobj, nextoffset = self.findNextCaretInOrder(nextobj, nextoffset)
+
+        if skipSpace:
+            text = self.queryNonEmptyText(nextobj)
+            while text and text.getText(nextoffset, nextoffset + 1).isspace():
+                nextobj, nextoffset = self.findNextCaretInOrder(nextobj, nextoffset)
+                text = self.queryNonEmptyText(nextobj)
+
+        return nextobj, nextoffset
+
+    def previousContext(self, obj=None, offset=-1, skipSpace=False):
+        if not obj:
+            obj, offset = self.getCaretContext()
+
+        prevobj, prevoffset = self.findPreviousCaretInOrder(obj, offset)
+        if (obj, offset) == (prevobj, prevoffset):
+            prevobj, prevoffset = self.findPreviousCaretInOrder(prevobj, prevoffset)
+
+        if skipSpace:
+            text = self.queryNonEmptyText(prevobj)
+            while text and text.getText(prevoffset, prevoffset + 1).isspace():
+                prevobj, prevoffset = self.findPreviousCaretInOrder(prevobj, prevoffset)
+                text = self.queryNonEmptyText(prevobj)
+
+        return prevobj, prevoffset
+
+    def contextsAreOnSameLine(self, a, b):
+        if a == b:
+            return True
+
+        aObj, aOffset = a
+        bObj, bOffset = b
+        aExtents = self.getExtents(aObj, aOffset, aOffset + 1)
+        bExtents = self.getExtents(bObj, bOffset, bOffset + 1)
+        return self.extentsAreOnSameLine(aExtents, bExtents)
+
+    @staticmethod
+    def extentsAreOnSameLine(a, b, pixelDelta=5):
+        if a == b:
+            return True
+
+        aX, aY, aWidth, aHeight = a
+        bX, bY, bWidth, bHeight = b
+
+        if aWidth == 0 and aHeight == 0:
+            return bY <= aY <= bY + bHeight
+        if bWidth == 0 and bHeight == 0:
+            return aY <= bY <= aY + aHeight
+
+        highestBottom = min(aY + aHeight, bY + bHeight)
+        lowestTop = max(aY, bY)
+        if lowestTop >= highestBottom:
+            return False
+
+        aMiddle = aY + aHeight / 2
+        bMiddle = bY + bHeight / 2
+        if abs(aMiddle - bMiddle) > pixelDelta:
+            return False
+
+        return True
+
+    @staticmethod
+    def getExtents(obj, startOffset, endOffset):
+        if not obj:
+            return [0, 0, 0, 0]
+
+        try:
+            text = obj.queryText()
+            if text.characterCount:
+                return list(text.getRangeExtents(startOffset, endOffset, 0))
+        except NotImplementedError:
+            pass
+        except:
+            return [0, 0, 0, 0]
+
+        role = obj.getRole()
+        parentRole = obj.parent.getRole()
+        if role in [pyatspi.ROLE_MENU, pyatspi.ROLE_LIST_ITEM] \
+           and parentRole in [pyatspi.ROLE_COMBO_BOX, pyatspi.ROLE_LIST_BOX]:
+            try:
+                ext = obj.parent.queryComponent().getExtents(0)
+            except:
+                return [0, 0, 0, 0]
+        else:
+            try:
+                ext = obj.queryComponent().getExtents(0)
+            except:
+                return [0, 0, 0, 0]
+
+        return [ext.x, ext.y, ext.width, ext.height]
+
+    def expandEOCs(self, obj, startOffset=0, endOffset=-1):
+        if not self.inDocumentContent(obj):
+            return ""
+
+        text = self.queryNonEmptyText(obj)
+        if not text:
+            return ""
+
+        string = text.getText(startOffset, endOffset)
+
+        if self.EMBEDDED_OBJECT_CHARACTER in string:
+            # If we're not getting the full text of this object, but
+            # rather a substring, we need to figure out the offset of
+            # the first child within this substring.
+            childOffset = 0
+            for child in obj:
+                if self.characterOffsetInParent(child) >= startOffset:
+                    break
+                childOffset += 1
+
+            toBuild = list(string)
+            count = toBuild.count(self.EMBEDDED_OBJECT_CHARACTER)
+            for i in range(count):
+                index = toBuild.index(self.EMBEDDED_OBJECT_CHARACTER)
+                try:
+                    child = obj[i + childOffset]
+                except:
+                    continue
+                childText = self.expandEOCs(child)
+                if not childText:
+                    childText = ""
+                toBuild[index] = "%s " % childText
+
+            string = "".join(toBuild).strip()
+
+        return string
+
+    def substring(self, obj, startOffset, endOffset):
+        if not self.inDocumentContent(obj):
+            return super().substring(obj, startOffset, endOffset)
+
+        text = self.queryNonEmptyText(obj)
+        if text:
+            return text.getText(startOffset, endOffset)
+
+        return ""
+
+    def textAttributes(self, acc, offset, get_defaults=False):
+        attrsForObj = self._currentAttrs.get(hash(acc)) or {}
+        if offset in attrsForObj:
+            return attrsForObj.get(offset)
+
+        attrs = super().textAttributes(acc, offset, get_defaults)
+        self._currentAttrs[hash(acc)] = {offset:attrs}
+
+        return attrs
+
+    def findObjectInContents(self, obj, offset, contents):
+        if not obj or not contents:
+            return -1
+
+        offset = max(0, offset)
+        matches = [x for x in contents if x[0] == obj]
+        match = [x for x in matches if x[1] <= offset < x[2]]
+        if match and match[0] and match[0] in contents:
+            return contents.index(match[0])
+
+        return -1
+
+    def isNonEntryTextWidget(self, obj):
+        rv = self._isNonEntryTextWidget.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        roles = [pyatspi.ROLE_CHECK_BOX,
+                 pyatspi.ROLE_CHECK_MENU_ITEM,
+                 pyatspi.ROLE_MENU,
+                 pyatspi.ROLE_MENU_ITEM,
+                 pyatspi.ROLE_PAGE_TAB,
+                 pyatspi.ROLE_RADIO_MENU_ITEM,
+                 pyatspi.ROLE_RADIO_BUTTON,
+                 pyatspi.ROLE_PUSH_BUTTON,
+                 pyatspi.ROLE_TOGGLE_BUTTON]
+
+        role = obj.getRole()
+        if role in roles:
+            rv = True
+        elif role in [pyatspi.ROLE_LIST_ITEM, pyatspi.ROLE_TABLE_CELL]:
+            rv = not self.isTextBlockElement(obj)
+
+        self._isNonEntryTextWidget[hash(obj)] = rv
+        return rv
+
+    def queryNonEmptyText(self, obj, excludeNonEntryTextWidgets=True):
+        if hash(obj) in self._text:
+            return self._text.get(hash(obj))
+
+        try:
+            rv = obj.queryText()
+            characterCount = rv.characterCount
+        except:
+            rv = None
+        else:
+            if not characterCount:
+                rv = None
+
+        if not self.isLiveRegion(obj):
+            doNotQuery = [pyatspi.ROLE_LIST,
+                          pyatspi.ROLE_TABLE_ROW,
+                          pyatspi.ROLE_TOOL_BAR]
+            if rv and obj.getRole() in doNotQuery:
+                rv = None
+            if rv and excludeNonEntryTextWidgets and self.isNonEntryTextWidget(obj):
+                rv = None
+            if rv and (self.isHidden(obj) or self.isOffScreenLabel(obj)):
+                rv = None
+
+        self._text[hash(obj)] = rv
+        return rv
+
+    def _treatTextObjectAsWhole(self, obj):
+        roles = [pyatspi.ROLE_CHECK_BOX,
+                 pyatspi.ROLE_CHECK_MENU_ITEM,
+                 pyatspi.ROLE_MENU,
+                 pyatspi.ROLE_MENU_ITEM,
+                 pyatspi.ROLE_RADIO_MENU_ITEM,
+                 pyatspi.ROLE_RADIO_BUTTON,
+                 pyatspi.ROLE_PUSH_BUTTON,
+                 pyatspi.ROLE_TOGGLE_BUTTON]
+
+        role = obj.getRole()
+        if role in roles:
+            return True
+
+        if role == pyatspi.ROLE_TABLE_CELL and self.isFocusModeWidget(obj):
+            return True
+
+        return False
+
+    def __findRange(self, text, offset, start, end, boundary):
+        # We should not have to do any of this. Seriously. This is why
+        # We can't have nice things.
+
+        allText = text.getText(0, -1)
+        extents = list(text.getRangeExtents(offset, offset + 1, 0))
+
+        def _inThisSpan(span):
+            return span[0] <= offset <= span[1]
+
+        def _onThisLine(span):
+            rangeExtents = list(text.getRangeExtents(span[0], span[0] + 1, 0))
+            return self.extentsAreOnSameLine(extents, rangeExtents)
+
+        spans = []
+        charCount = text.characterCount
+        if boundary == pyatspi.TEXT_BOUNDARY_SENTENCE_START:
+            spans = [m.span() for m in re.finditer("\S*[^\.\?\!]+((?<!\w)[\.\?\!]+(?!\w)|\S*)", allText)]
+        elif boundary is not None:
+            spans = [m.span() for m in re.finditer("[^\n\r]+", allText)]
+        if not spans:
+            spans = [(0, charCount)]
+
+        rangeStart, rangeEnd = 0, charCount
+        for span in spans:
+            if _inThisSpan(span):
+                rangeStart, rangeEnd = span[0], span[1] + 1
+                break
+
+        string = allText[rangeStart:rangeEnd]
+        if string and boundary in [pyatspi.TEXT_BOUNDARY_SENTENCE_START, None]:
+            return string, rangeStart, rangeEnd
+
+        words = [m.span() for m in re.finditer("[^\s\ufffc]+", string)]
+        words = list(map(lambda x: (x[0] + rangeStart, x[1] + rangeStart), words))
+        if boundary == pyatspi.TEXT_BOUNDARY_WORD_START:
+            spans = list(filter(_inThisSpan, words))
+        if boundary == pyatspi.TEXT_BOUNDARY_LINE_START:
+            spans = list(filter(_onThisLine, words))
+        if spans:
+            rangeStart, rangeEnd = spans[0][0], spans[-1][1] + 1
+            string = allText[rangeStart:rangeEnd]
+
+        return string, rangeStart, rangeEnd
+
+    def _getTextAtOffset(self, obj, offset, boundary):
+        if not obj:
+            msg = "INFO: Results for text at offset %i for %s using %s:\n" \
+                  "      String: '', Start: 0, End: 0. (obj is None)" % (offset, obj, boundary)
+            debug.println(debug.LEVEL_INFO, msg)
+            return '', 0, 0
+
+        text = self.queryNonEmptyText(obj)
+        if not text:
+            msg = "INFO: Results for text at offset %i for %s using %s:\n" \
+                  "      String: '', Start: 0, End: 1. (queryNonEmptyText() returned None)" \
+                  % (offset, obj, boundary)
+            debug.println(debug.LEVEL_INFO, msg)
+            return '', 0, 1
+
+        if boundary == pyatspi.TEXT_BOUNDARY_CHAR:
+            string, start, end = text.getText(offset, offset + 1), offset, offset + 1
+            s = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+            msg = "INFO: Results for text at offset %i for %s using %s:\n" \
+                  "      String: '%s', Start: %i, End: %i." % (offset, obj, boundary, s, start, end)
+            debug.println(debug.LEVEL_INFO, msg)
+            return string, start, end
+
+        if not boundary:
+            string, start, end = text.getText(offset, -1), offset, text.characterCount
+            s = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+            msg = "INFO: Results for text at offset %i for %s using %s:\n" \
+                  "      String: '%s', Start: %i, End: %i." % (offset, obj, boundary, s, start, end)
+            debug.println(debug.LEVEL_INFO, msg)
+            return string, start, end
+
+        if boundary == pyatspi.TEXT_BOUNDARY_SENTENCE_START \
+            and not obj.getState().contains(pyatspi.STATE_EDITABLE):
+            allText = text.getText(0, -1)
+            if obj.getRole() in [pyatspi.ROLE_LIST_ITEM, pyatspi.ROLE_HEADING] \
+               or not (re.search("\w", allText) and self.isTextBlockElement(obj)):
+                string, start, end = allText, 0, text.characterCount
+                s = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+                msg = "INFO: Results for text at offset %i for %s using %s:\n" \
+                      "      String: '%s', Start: %i, End: %i." % (offset, obj, boundary, s, start, end)
+                debug.println(debug.LEVEL_INFO, msg)
+                return string, start, end
+
+        offset = max(0, offset)
+        string, start, end = text.getTextAtOffset(offset, boundary)
+
+        # The above should be all that we need to do, but....
+
+        needSadHack = False
+        testString, testStart, testEnd = text.getTextAtOffset(start, boundary)
+        if (string, start, end) != (testString, testStart, testEnd):
+            s1 = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+            s2 = testString.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+            msg = "FAIL: Bad results for text at offset for %s using %s.\n" \
+                  "      For offset %i - String: '%s', Start: %i, End: %i.\n" \
+                  "      For offset %i - String: '%s', Start: %i, End: %i.\n" \
+                  "      The bug is the above results should be the same.\n" \
+                  "      This very likely needs to be fixed by the toolkit." \
+                  % (obj, boundary, offset, s1, start, end, start, s2, testStart, testEnd)
+            debug.println(debug.LEVEL_INFO, msg)
+            needSadHack = True
+        elif not string and 0 <= offset < text.characterCount:
+            s1 = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+            s2 = text.getText(0, -1).replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+            msg = "FAIL: Bad results for text at offset %i for %s using %s:\n" \
+                  "      String: '%s', Start: %i, End: %i.\n" \
+                  "      The bug is no text reported for a valid offset.\n" \
+                  "      Character count: %i, Full text: '%s'.\n" \
+                  "      This very likely needs to be fixed by the toolkit." \
+                  % (offset, obj, boundary, s1, start, end, text.characterCount, s2)
+            debug.println(debug.LEVEL_INFO, msg)
+            needSadHack = True
+        elif not (start <= offset < end):
+            s1 = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+            msg = "FAIL: Bad results for text at offset %i for %s using %s:\n" \
+                  "      String: '%s', Start: %i, End: %i.\n" \
+                  "      The bug is the range returned is outside of the offset.\n" \
+                  "      This very likely needs to be fixed by the toolkit." \
+                  % (offset, obj, boundary, s1, start, end)
+            debug.println(debug.LEVEL_INFO, msg)
+            needSadHack = True
+
+        if needSadHack:
+            sadString, sadStart, sadEnd = self.__findRange(text, offset, start, end, boundary)
+            s = sadString.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+            msg = "HACK: Attempting to recover from above failure.\n" \
+                  "      String: '%s', Start: %i, End: %i." % (s, sadStart, sadEnd)
+            debug.println(debug.LEVEL_INFO, msg)
+            return sadString, sadStart, sadEnd
+
+        s = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
+        msg = "INFO: Results for text at offset %i for %s using %s:\n" \
+              "      String: '%s', Start: %i, End: %i." % (offset, obj, boundary, s, start, end)
+        debug.println(debug.LEVEL_INFO, msg)
+        return string, start, end
+
+    def _getContentsForObj(self, obj, offset, boundary):
+        if not obj:
+            return []
+
+        string, start, end = self._getTextAtOffset(obj, offset, boundary)
+        if not string:
+            return [[obj, start, end, string]]
+
+        stringOffset = offset - start
+        try:
+            char = string[stringOffset]
+        except:
+            pass
+        else:
+            if char == self.EMBEDDED_OBJECT_CHARACTER:
+                childIndex = self.getChildIndex(obj, offset)
+                try:
+                    child = obj[childIndex]
+                except:
+                    pass
+                else:
+                    return self._getContentsForObj(child, 0, boundary)
+
+        ranges = [m.span() for m in re.finditer("[^\ufffc]+", string)]
+        strings = list(filter(lambda x: x[0] <= stringOffset <= x[1], ranges))
+        if len(strings) == 1:
+            rangeStart, rangeEnd = strings[0]
+            start += rangeStart
+            string = string[rangeStart:rangeEnd]
+            end = start + len(string)
+
+        return [[obj, start, end, string]]
+
+    def getSentenceContentsAtOffset(self, obj, offset, useCache=True):
+        if not obj:
+            return []
+
+        offset = max(0, offset)
+
+        if useCache:
+            if self.findObjectInContents(obj, offset, self._currentSentenceContents) != -1:
+                return self._currentSentenceContents
+
+        boundary = pyatspi.TEXT_BOUNDARY_SENTENCE_START
+        objects = self._getContentsForObj(obj, offset, boundary)
+        state = obj.getState()
+        if state.contains(pyatspi.STATE_EDITABLE) \
+           and state.contains(pyatspi.STATE_FOCUSED):
+            return objects
+
+        def _treatAsSentenceEnd(x):
+            xObj, xStart, xEnd, xString = x
+            if not self.isTextBlockElement(xObj):
+                return False
+
+            text = self.queryNonEmptyText(xObj)
+            if text and 0 < text.characterCount <= xEnd:
+                return True
+
+            if 0 <= xStart <= 5:
+                xString = " ".join(xString.split()[1:])
+
+            match = re.search("\S[\.\!\?]+(\s|\Z)", xString)
+            return match is not None
+
+        # Check for things in the same sentence before this object.
+        firstObj, firstStart, firstEnd, firstString = objects[0]
+        while firstObj and firstString:
+            if firstStart == 0 and self.isTextBlockElement(firstObj):
+                break
+
+            prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
+            onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
+            onLeft = list(filter(lambda x: x not in objects, onLeft))
+            endsOnLeft = list(filter(_treatAsSentenceEnd, onLeft))
+            if endsOnLeft:
+                i = onLeft.index(endsOnLeft[-1])
+                onLeft = onLeft[i+1:]
+
+            if not onLeft:
+                break
+
+            objects[0:0] = onLeft
+            firstObj, firstStart, firstEnd, firstString = objects[0]
+
+        # Check for things in the same sentence after this object.
+        while not _treatAsSentenceEnd(objects[-1]):
+            lastObj, lastStart, lastEnd, lastString = objects[-1]
+            nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
+            onRight = self._getContentsForObj(nextObj, nOffset, boundary)
+            onRight = list(filter(lambda x: x not in objects, onRight))
+            if not onRight:
+                break
+
+            objects.extend(onRight)
+
+        if useCache:
+            self._currentSentenceContents = objects
+
+        return objects
+
+    def getCharacterAtOffset(self, obj, offset):
+        text = self.queryNonEmptyText(obj)
+        if text:
+            return text.getText(offset, offset + 1)
+
+        return ""
+
+    def getCharacterContentsAtOffset(self, obj, offset, useCache=True):
+        if not obj:
+            return []
+
+        offset = max(0, offset)
+
+        if useCache:
+            if self.findObjectInContents(obj, offset, self._currentCharacterContents) != -1:
+                return self._currentCharacterContents
+
+        boundary = pyatspi.TEXT_BOUNDARY_CHAR
+        objects = self._getContentsForObj(obj, offset, boundary)
+        if useCache:
+            self._currentCharacterContents = objects
+
+        return objects
+
+    def getWordContentsAtOffset(self, obj, offset, useCache=True):
+        if not obj:
+            return []
+
+        offset = max(0, offset)
+
+        if useCache:
+            if self.findObjectInContents(obj, offset, self._currentWordContents) != -1:
+                return self._currentWordContents
+
+        boundary = pyatspi.TEXT_BOUNDARY_WORD_START
+        objects = self._getContentsForObj(obj, offset, boundary)
+        extents = self.getExtents(obj, offset, offset + 1)
+
+        def _include(x):
+            if x in objects:
+                return False
+
+            xObj, xStart, xEnd, xString = x
+            if xStart == xEnd or not xString:
+                return False
+
+            xExtents = self.getExtents(xObj, xStart, xStart + 1)
+            return self.extentsAreOnSameLine(extents, xExtents)
+
+        # Check for things in the same word to the left of this object.
+        firstObj, firstStart, firstEnd, firstString = objects[0]
+        prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
+        while prevObj and firstString:
+            text = self.queryNonEmptyText(prevObj)
+            if not text or text.getText(pOffset, pOffset + 1).isspace():
+                break
+
+            onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
+            onLeft = list(filter(_include, onLeft))
+            if not onLeft:
+                break
+
+            objects[0:0] = onLeft
+            firstObj, firstStart, firstEnd, firstString = objects[0]
+            prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
+
+        # Check for things in the same word to the right of this object.
+        lastObj, lastStart, lastEnd, lastString = objects[-1]
+        while lastObj and lastString and not lastString[-1].isspace():
+            nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
+            onRight = self._getContentsForObj(nextObj, nOffset, boundary)
+            onRight = list(filter(_include, onRight))
+            if not onRight:
+                break
+
+            objects.extend(onRight)
+            lastObj, lastStart, lastEnd, lastString = objects[-1]
+
+        # We want to treat the list item marker as its own word.
+        firstObj, firstStart, firstEnd, firstString = objects[0]
+        if firstStart == 0 and firstObj.getRole() == pyatspi.ROLE_LIST_ITEM:
+            objects = [objects[0]]
+
+        if useCache:
+            self._currentWordContents = objects
+
+        return objects
+
+    def getObjectContentsAtOffset(self, obj, offset=0, useCache=True):
+        if not obj:
+            return []
+
+        offset = max(0, offset)
+
+        if useCache:
+            if self.findObjectInContents(obj, offset, self._currentObjectContents) != -1:
+                return self._currentObjectContents
+
+        objIsLandmark = self.isLandmark(obj)
+
+        def _isInObject(x):
+            if not x:
+                return False
+            if x == obj:
+                return True
+            return _isInObject(x.parent)
+
+        def _include(x):
+            if x in objects:
+                return False
+
+            xObj, xStart, xEnd, xString = x
+            if xStart == xEnd:
+                return False
+
+            if objIsLandmark and self.isLandmark(xObj) and obj != xObj:
+                return False
+
+            return _isInObject(xObj)
+
+        objects = self._getContentsForObj(obj, offset, None)
+        lastObj, lastStart, lastEnd, lastString = objects[-1]
+        nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
+        while nextObj:
+            onRight = self._getContentsForObj(nextObj, nOffset, None)
+            onRight = list(filter(_include, onRight))
+            if not onRight:
+                break
+
+            objects.extend(onRight)
+            lastObj, lastEnd = objects[-1][0], objects[-1][2]
+            nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
+
+        if useCache:
+            self._currentObjectContents = objects
+
+        return objects
+
+    def _contentIsSubsetOf(self, contentA, contentB):
+        objA, startA, endA, stringA = contentA
+        objB, startB, endB, stringB = contentB
+        if objA == objB:
+            setA = set(range(startA, endA))
+            setB = set(range(startB, endB))
+            return setA.issubset(setB)
+
+        return False
+
+    def getLineContentsAtOffset(self, obj, offset, layoutMode=None, useCache=True):
+        if not obj:
+            return []
+
+        text = self.queryNonEmptyText(obj)
+        if text and offset == text.characterCount:
+            offset -= 1
+        offset = max(0, offset)
+
+        if useCache:
+            if self.findObjectInContents(obj, offset, self._currentLineContents) != -1:
+                return self._currentLineContents
+
+        if layoutMode == None:
+            layoutMode = _settingsManager.getSetting('layoutMode')
+
+        objects = []
+        extents = self.getExtents(obj, offset, offset + 1)
+
+        def _include(x):
+            if x in objects:
+                return False
+
+            xObj, xStart, xEnd, xString = x
+            if xStart == xEnd:
+                return False
+
+            xExtents = self.getExtents(xObj, xStart, xStart + 1)
+            return self.extentsAreOnSameLine(extents, xExtents)
+
+        boundary = pyatspi.TEXT_BOUNDARY_LINE_START
+        objects = self._getContentsForObj(obj, offset, boundary)
+
+        firstObj, firstStart, firstEnd, firstString = objects[0]
+        if extents[2] == 0 and extents[3] == 0:
+            extents = self.getExtents(obj, firstStart, firstEnd)
+
+        lastObj, lastStart, lastEnd, lastString = objects[-1]
+        prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
+        nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
+        if not layoutMode:
+            if firstString and not re.search("\w", firstString) \
+               and (re.match("[^\w\s]", firstString[0]) or not firstString.strip()):
+                onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
+                onLeft = list(filter(_include, onLeft))
+                objects[0:0] = onLeft
+
+            text = self.queryNonEmptyText(nextObj)
+            if text:
+                char = text.getText(nOffset, nOffset + 1)
+                if re.match("[^\w\s]", char):
+                    objects.append([nextObj, nOffset, nOffset + 1, char])
+
+            if useCache:
+                self._currentLineContents = objects
+
+            return objects
+
+        # Check for things on the same line to the left of this object.
+        while prevObj:
+            text = self.queryNonEmptyText(prevObj)
+            if text and text.getText(pOffset, pOffset + 1) in [" ", "\xa0"]:
+                prevObj, pOffset = self.findPreviousCaretInOrder(prevObj, pOffset)
+
+            onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
+            onLeft = list(filter(_include, onLeft))
+            if not onLeft:
+                break
+
+            if self._contentIsSubsetOf(objects[0], onLeft[-1]):
+                objects.pop(0)
+
+            objects[0:0] = onLeft
+            firstObj, firstStart = objects[0][0], objects[0][1]
+            prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
+
+        # Check for things on the same line to the right of this object.
+        while nextObj:
+            text = self.queryNonEmptyText(nextObj)
+            if text and text.getText(nOffset, nOffset + 1) in [" ", "\xa0"]:
+                nextObj, nOffset = self.findNextCaretInOrder(nextObj, nOffset)
+
+            onRight = self._getContentsForObj(nextObj, nOffset, boundary)
+            onRight = list(filter(_include, onRight))
+            if not onRight:
+                break
+
+            objects.extend(onRight)
+            lastObj, lastEnd = objects[-1][0], objects[-1][2]
+            nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
+
+        if useCache:
+            self._currentLineContents = objects
+
+        return objects
+
+    def justEnteredObject(self, obj, startOffset, endOffset):
+        lastKey, mods = self.lastKeyAndModifiers()
+        if (lastKey == "Down" and not mods) or self._script.inSayAll():
+            return startOffset == 0
+
+        if lastKey == "Up" and not mods:
+            text = self.queryNonEmptyText(obj)
+            if not text:
+                return True
+            return endOffset == text.characterCount
+
+        return True
+
+    def isFocusModeWidget(self, obj):
+        try:
+            role = obj.getRole()
+            state = obj.getState()
+        except:
+            return False
+
+        if state.contains(pyatspi.STATE_EDITABLE) \
+           or state.contains(pyatspi.STATE_EXPANDABLE):
+            return True
+
+        focusModeRoles = [pyatspi.ROLE_COMBO_BOX,
+                          pyatspi.ROLE_ENTRY,
+                          pyatspi.ROLE_LIST_BOX,
+                          pyatspi.ROLE_LIST_ITEM,
+                          pyatspi.ROLE_MENU,
+                          pyatspi.ROLE_MENU_ITEM,
+                          pyatspi.ROLE_CHECK_MENU_ITEM,
+                          pyatspi.ROLE_RADIO_MENU_ITEM,
+                          pyatspi.ROLE_PAGE_TAB,
+                          pyatspi.ROLE_PASSWORD_TEXT,
+                          pyatspi.ROLE_PROGRESS_BAR,
+                          pyatspi.ROLE_SLIDER,
+                          pyatspi.ROLE_SPIN_BUTTON,
+                          pyatspi.ROLE_TOOL_BAR,
+                          pyatspi.ROLE_TABLE_CELL,
+                          pyatspi.ROLE_TABLE_ROW,
+                          pyatspi.ROLE_TABLE,
+                          pyatspi.ROLE_TREE_TABLE,
+                          pyatspi.ROLE_TREE]
+
+        if role in focusModeRoles \
+           and not self.isTextBlockElement(obj):
+            return True
+
+        if self.isGridDescendant(obj):
+            return True
+
+        return False
+
+    def isTextBlockElement(self, obj):
+        if not obj:
+            return False
+
+        rv = self._isTextBlockElement.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        role = obj.getRole()
+        state = obj.getState()
+
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        textBlockElements = [pyatspi.ROLE_CAPTION,
+                             pyatspi.ROLE_COLUMN_HEADER,
+                             pyatspi.ROLE_DOCUMENT_FRAME,
+                             pyatspi.ROLE_DOCUMENT_WEB,
+                             pyatspi.ROLE_FOOTER,
+                             pyatspi.ROLE_FORM,
+                             pyatspi.ROLE_HEADING,
+                             pyatspi.ROLE_LABEL,
+                             pyatspi.ROLE_LIST_ITEM,
+                             pyatspi.ROLE_PANEL,
+                             pyatspi.ROLE_PARAGRAPH,
+                             pyatspi.ROLE_ROW_HEADER,
+                             pyatspi.ROLE_SECTION,
+                             pyatspi.ROLE_TEXT,
+                             pyatspi.ROLE_TABLE_CELL]
+
+        if not self.inDocumentContent(obj):
+            rv = False
+        elif not role in textBlockElements:
+            rv = False
+        elif state.contains(pyatspi.STATE_EDITABLE):
+            rv = False
+        elif role in [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_DOCUMENT_WEB]:
+            rv = True
+        elif not state.contains(pyatspi.STATE_FOCUSABLE) and not state.contains(pyatspi.STATE_FOCUSED):
+            rv = True
+        else:
+            rv = False
+
+        self._isTextBlockElement[hash(obj)] = rv
+        return rv
+
+    def filterContentsForPresentation(self, contents, inferLabels=False):
+        def _include(x):
+            obj, start, end, string = x
+            if not obj:
+                return False
+
+            if (self.isTextBlockElement(obj) and not string.strip()) \
+               or self.isAnchor(obj) \
+               or self.hasNoSize(obj) \
+               or self.isOffScreenLabel(obj) \
+               or self.isLabellingContents(x, contents):
+                return False
+
+            widget = self.isInferredLabelForContents(x, contents)
+            alwaysFilter = [pyatspi.ROLE_RADIO_BUTTON, pyatspi.ROLE_CHECK_BOX]
+            if widget and (inferLabels or widget.getRole() in alwaysFilter):
+                return False
+
+            return True
+
+        return list(filter(_include, contents))
+
+    def needsSeparator(self, lastChar, nextChar):
+        if lastChar.isspace() or nextChar.isspace():
+            return False
+
+        openingPunctuation = ["(", "[", "{", "<"]
+        closingPunctuation = [".", "?", "!", ":", ",", ";", ")", "]", "}", ">"]
+        if lastChar in closingPunctuation or nextChar in openingPunctuation:
+            return True
+        if lastChar in openingPunctuation or nextChar in closingPunctuation:
+            return False
+
+        return lastChar.isalnum()
+
+    def supportsSelectionAndTable(self, obj):
+        interfaces = pyatspi.listInterfaces(obj)
+        return 'Table' in interfaces and 'Selection' in interfaces
+
+    def isGridDescendant(self, obj):
+        if not obj:
+            return False
+
+        rv = self._isGridDescendant.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        rv = pyatspi.findAncestor(obj, self.supportsSelectionAndTable) is not None
+        self._isGridDescendant[hash(obj)] = rv
+        return rv
+
+    def isOffScreenLabel(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        rv = self._isOffScreenLabel.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        rv = False
+        isLabelFor = lambda x: x.getRelationType() == pyatspi.RELATION_LABEL_FOR
+        try:
+            relationSet = obj.getRelationSet()
+        except:
+            pass
+        else:
+            relations = list(filter(isLabelFor, relationSet))
+            if relations:
+                try:
+                    text = obj.queryText()
+                    end = text.characterCount
+                except:
+                    end = 1
+                x, y, width, height = self.getExtents(obj, 0, end)
+                if x < 0 or y < 0:
+                    rv = True
+
+        self._isOffScreenLabel[hash(obj)] = rv
+        return rv
+
+    def isInferredLabelForContents(self, content, contents):
+        obj, start, end, string = content
+        objs = list(filter(self.shouldInferLabelFor, [x[0] for x in contents]))
+        if not objs:
+            return None
+
+        for o in objs:
+            label, sources = self.inferLabelFor(o)
+            if obj in sources and label.strip() == string.strip():
+                return o
+
+        return None
+
+    def isLabellingContents(self, content, contents):
+        obj, start, end, string = content
+        if obj.getRole() != pyatspi.ROLE_LABEL:
+            return None
+
+        relationSet = obj.getRelationSet()
+        if not relationSet:
+            return None
+
+        for relation in relationSet:
+            if relation.getRelationType() \
+                == pyatspi.RELATION_LABEL_FOR:
+                for i in range(0, relation.getNTargets()):
+                    target = relation.getTarget(i)
+                    for content in contents:
+                        if content[0] == target:
+                            return target
+
+        return None
+
+    def isAnchor(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        rv = self._isAnchor.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        rv = False
+        if obj.getRole() == pyatspi.ROLE_LINK \
+           and not obj.getState().contains(pyatspi.STATE_FOCUSABLE) \
+           and not 'Action' in pyatspi.listInterfaces(obj) \
+           and not self.queryNonEmptyText(obj):
+            rv = True
+
+        self._isAnchor[hash(obj)] = rv
+        return rv
+
+    def isClickableElement(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        rv = self._isClickableElement.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        rv = False
+        if not obj.getState().contains(pyatspi.STATE_FOCUSABLE) \
+           and not self.isFocusModeWidget(obj):
+            try:
+                action = obj.queryAction()
+                names = [action.getName(i) for i in range(action.nActions)]
+            except NotImplementedError:
+                rv = False
+            else:
+                rv = "click" in names
+
+        self._isClickableElement[hash(obj)] = rv
+        return rv
+
+    def isLandmark(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        rv = self._isLandmark.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        if obj.getRole() == pyatspi.ROLE_LANDMARK:
+            rv = True
+        else:
+            try:
+                attrs = dict([attr.split(':', 1) for attr in obj.getAttributes()])
+            except:
+                attrs = {}
+            rv = attrs.get('xml-roles') in settings.ariaLandmarks
+
+        self._isLandmark[hash(obj)] = rv
+        return rv
+
+    def isLiveRegion(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        rv = self._isLiveRegion.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        try:
+            attrs = dict([attr.split(':', 1) for attr in obj.getAttributes()])
+        except:
+            attrs = {}
+
+        rv = 'container-live' in attrs
+        self._isLiveRegion[hash(obj)] = rv
+        return rv
+
+    def isLink(self, obj):
+        if not obj:
+            return False
+
+        rv = self._isLink.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        role = obj.getRole()
+        if role == pyatspi.ROLE_LINK and not self.isAnchor(obj):
+            rv = True
+        elif role == pyatspi.ROLE_TEXT \
+           and obj.parent.getRole() == pyatspi.ROLE_LINK \
+           and obj.name and obj.name == obj.parent.name:
+            rv = True
+
+        self._isLink[hash(obj)] = rv
+        return rv
+
+    def isNonNavigablePopup(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        rv = self._isNonNavigablePopup.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        role = obj.getRole()
+        if role == pyatspi.ROLE_TOOL_TIP:
+            rv = True
+
+        self._isNonNavigablePopup[hash(obj)] = rv
+        return rv
+
+    def hasLongDesc(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        rv = self._hasLongDesc.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        try:
+            action = obj.queryAction()
+        except NotImplementedError:
+            rv = False
+        else:
+            names = [action.getName(i) for i in range(action.nActions)]
+            rv = "showlongdesc" in names
+
+        self._hasLongDesc[hash(obj)] = rv
+        return rv
+
+    def inferLabelFor(self, obj):
+        if not self.shouldInferLabelFor(obj):
+            return None, []
+
+        rv = self._inferredLabels.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        rv = self._script.labelInference.infer(obj, False)
+        self._inferredLabels[hash(obj)] = rv
+        return rv
+
+    def shouldInferLabelFor(self, obj):
+        if obj.name:
+            return False
+
+        if self._script.inSayAll():
+            return False
+
+        if not self.inDocumentContent():
+            return False
+
+        role = obj.getRole()
+
+        # TODO - JD: This is private.
+        if self._script._lastCommandWasCaretNav \
+           and role not in [pyatspi.ROLE_RADIO_BUTTON, pyatspi.ROLE_CHECK_BOX]:
+            return False
+
+        roles =  [pyatspi.ROLE_CHECK_BOX,
+                  pyatspi.ROLE_COMBO_BOX,
+                  pyatspi.ROLE_ENTRY,
+                  pyatspi.ROLE_LIST_BOX,
+                  pyatspi.ROLE_PASSWORD_TEXT,
+                  pyatspi.ROLE_RADIO_BUTTON]
+        if role not in roles:
+            return False
+
+        if self.displayedLabel(obj):
+            return False
+
+        return True
+
+    def eventIsStatusBarNoise(self, event):
+        if self.inDocumentContent(event.source):
+            return False
+
+        eType = event.type
+        if eType.startswith("object:text-") or eType.endswith("accessible-name"):
+            return event.source.getRole() == pyatspi.ROLE_STATUS_BAR
+
+        return False
+
+    def eventIsAutocompleteNoise(self, event):
+        if not self.inDocumentContent(event.source):
+            return False
+
+        isListBoxItem = lambda x: x and x.parent and x.parent.getRole() == pyatspi.ROLE_LIST_BOX
+        isMenuItem = lambda x: x and x.parent and x.parent.getRole() == pyatspi.ROLE_MENU
+        isComboBoxItem = lambda x: x and x.parent and x.parent.getRole() == pyatspi.ROLE_COMBO_BOX
+
+        if event.source.getState().contains(pyatspi.STATE_EDITABLE) \
+           and event.type.startswith("object:text-"):
+            obj, offset = self.getCaretContext()
+            if isListBoxItem(obj) or isMenuItem(obj):
+                return True
+
+            if obj == event.source and isComboBoxItem(obj):
+                lastKey, mods = self.lastKeyAndModifiers()
+                if lastKey in ["Down", "Up"]:
+                    return True
+
+        return False
+
+    def textEventIsDueToInsertion(self, event):
+        if not event.type.startswith("object:text-"):
+            return False
+
+        if not self.inDocumentContent(event.source) \
+           or not event.source.getState().contains(pyatspi.STATE_EDITABLE) \
+           or not event.source == orca_state.locusOfFocus:
+            return False
+
+        if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
+            inputEvent = orca_state.lastNonModifierKeyEvent
+            return inputEvent and inputEvent.isPrintableKey()
+
+        return False
+
+    def textEventIsForNonNavigableTextObject(self, event):
+        if not event.type.startswith("object:text-"):
+            return False
+
+        return self._treatTextObjectAsWhole(event.source)
+
+    # TODO - JD: As an experiment, we're stopping these at the event manager.
+    # If that works, this can be removed.
+    def eventIsEOCAdded(self, event):
+        if not self.inDocumentContent(event.source):
+            return False
+
+        if event.type.startswith("object:text-changed:insert"):
+            return self.EMBEDDED_OBJECT_CHARACTER in event.any_data
+
+        return False
+
+    def caretMovedToSamePageFragment(self, event):
+        if not event.type.startswith("object:text-caret-moved"):
+            return False
+
+        linkURI = self.uri(orca_state.locusOfFocus)
+        docURI = self.documentFrameURI()
+        if linkURI == docURI:
+            return True
+
+        return False
+
+    @staticmethod
+    def getHyperlinkRange(obj):
+        try:
+            hyperlink = obj.queryHyperlink()
+            start, end = hyperlink.startIndex, hyperlink.endIndex
+        except:
+            return -1, -1
+
+        return start, end
+
+    def characterOffsetInParent(self, obj):
+        start, end, length = self._rangeInParentWithLength(obj)
+        return start
+
+    def _rangeInParentWithLength(self, obj):
+        if not obj:
+            return -1, -1, 0
+
+        text = self.queryNonEmptyText(obj.parent)
+        if not text:
+            return -1, -1, 0
+
+        start, end = self.getHyperlinkRange(obj)
+        return start, end, text.characterCount
+
+    @staticmethod
+    def getChildIndex(obj, offset):
+        try:
+            hypertext = obj.queryHypertext()
+        except:
+            return -1
+
+        return hypertext.getLinkIndex(offset)
+
+    def getChildAtOffset(self, obj, offset):
+        index = self.getChildIndex(obj, offset)
+        if index == -1:
+            return None
+
+        try:
+            child = obj[index]
+        except:
+            return None
+
+        return child
+
+    def hasNoSize(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        rv = self._hasNoSize.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        try:
+            extents = obj.queryComponent().getExtents(0)
+        except:
+            rv = True
+        else:
+            rv = not (extents.width and extents.height)
+
+        self._hasNoSize[hash(obj)] = rv
+        return rv
+
+    def doNotDescendForCaret(self, obj):
+        if not obj or self.isZombie(obj):
+            return True
+
+        if self.isHidden(obj) or self.isOffScreenLabel(obj):
+            return True
+
+        if self.isTextBlockElement(obj):
+            return False
+
+        doNotDescend = [pyatspi.ROLE_COMBO_BOX,
+                        pyatspi.ROLE_LIST_BOX,
+                        pyatspi.ROLE_MENU_BAR,
+                        pyatspi.ROLE_MENU,
+                        pyatspi.ROLE_MENU_ITEM,
+                        pyatspi.ROLE_PUSH_BUTTON,
+                        pyatspi.ROLE_TOGGLE_BUTTON,
+                        pyatspi.ROLE_TOOL_BAR,
+                        pyatspi.ROLE_TOOL_TIP,
+                        pyatspi.ROLE_TREE,
+                        pyatspi.ROLE_TREE_TABLE]
+        return obj.getRole() in doNotDescend
+
+    def _searchForCaretContext(self, obj):
+        context = [None, -1]
+        while obj:
+            try:
+                offset = obj.queryText().caretOffset
+            except:
+                obj = None
+            else:
+                context = [obj, offset]
+                childIndex = self.getChildIndex(obj, offset)
+                if childIndex >= 0 and obj.childCount:
+                    obj = obj[childIndex]
+                else:
+                    break
+
+        return context
+
+    def _getCaretContextViaLocusOfFocus(self):
+        obj = orca_state.locusOfFocus
+        try:
+            offset = obj.queryText().caretOffset
+        except NotImplementedError:
+            offset = 0
+        except:
+            offset = -1
+
+        return obj, offset
+
+    def getCaretContext(self, documentFrame=None):
+        documentFrame = documentFrame or self.documentFrame()
+        if not documentFrame:
+            return self._getCaretContextViaLocusOfFocus()
+
+        context = self._caretContexts.get(hash(documentFrame.parent))
+        if context:
+            return context
+
+        obj, offset = self._searchForCaretContext(documentFrame)
+        obj, offset = self.findNextCaretInOrder(obj, max(-1, offset - 1))
+        self.setCaretContext(obj, offset, documentFrame)
+
+        return obj, offset
+
+    def clearCaretContext(self, documentFrame=None):
+        self.clearContentCache()
+        documentFrame = documentFrame or self.documentFrame()
+        if not documentFrame:
+            return
+
+        parent = documentFrame.parent
+        self._caretContexts.pop(hash(parent), None)
+
+    def setCaretContext(self, obj=None, offset=-1, documentFrame=None):
+        documentFrame = documentFrame or self.documentFrame()
+        if not documentFrame:
+            return
+
+        parent = documentFrame.parent
+        self._caretContexts[hash(parent)] = obj, offset
+
+    def findFirstCaretContext(self, obj, offset):
+        try:
+            role = obj.getRole()
+        except:
+            msg = "ERROR: Exception getting first caret context for %s %i" % (obj, offset)
+            debug.println(debug.LEVEL_INFO, msg)
+            return None, -1
+
+        lookInChild = [pyatspi.ROLE_LIST,
+                       pyatspi.ROLE_TABLE,
+                       pyatspi.ROLE_TABLE_ROW]
+        if role in lookInChild and obj.childCount:
+            msg = "INFO: First caret context for %s, %i will look in child %s" % (obj, offset, obj[0])
+            debug.println(debug.LEVEL_INFO, msg)
+            return self.findFirstCaretContext(obj[0], 0)
+
+        text = self.queryNonEmptyText(obj)
+        if not text:
+            if self.isTextBlockElement(obj) or self.isAnchor(obj):
+                nextObj, nextOffset = self.nextContext(obj, offset)
+                if nextObj:
+                    msg = "INFO: First caret context for %s, %i is %s, %i" % (obj, offset, nextObj, 
nextOffset)
+                    debug.println(debug.LEVEL_INFO, msg)
+                    return nextObj, nextOffset
+
+            msg = "INFO: First caret context for %s, %i is %s, %i" % (obj, offset, obj, 0)
+            debug.println(debug.LEVEL_INFO, msg)
+            return obj, 0
+
+        if offset >= text.characterCount:
+            msg = "INFO: First caret context for %s, %i is %s, %i" % (obj, offset, obj, text.characterCount)
+            debug.println(debug.LEVEL_INFO, msg)
+            return obj, text.characterCount
+
+        allText = text.getText(0, -1)
+        offset = max (0, offset)
+        if allText[offset] != self.EMBEDDED_OBJECT_CHARACTER:
+            msg = "INFO: First caret context for %s, %i is %s, %i" % (obj, offset, obj, offset)
+            debug.println(debug.LEVEL_INFO, msg)
+            return obj, offset
+
+        child = self.getChildAtOffset(obj, offset)
+        if not child:
+            msg = "INFO: First caret context for %s, %i is %s, %i" % (obj, offset, None, -1)
+            debug.println(debug.LEVEL_INFO, msg)
+            return None, -1
+
+        return self.findFirstCaretContext(child, 0)
+
+    def findNextCaretInOrder(self, obj=None, offset=-1):
+        if not obj:
+            obj, offset = self.getCaretContext()
+
+        if not obj or not self.inDocumentContent(obj):
+            return None, -1
+
+        if not (self.isHidden(obj) or self.isOffScreenLabel(obj) or self.isNonNavigablePopup(obj)):
+            text = self.queryNonEmptyText(obj)
+            if text:
+                allText = text.getText(0, -1)
+                for i in range(offset + 1, len(allText)):
+                    child = self.getChildAtOffset(obj, i)
+                    if child and not self.isZombie(child):
+                        return self.findNextCaretInOrder(child, -1)
+                    if allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
+                        return obj, i
+            elif obj.childCount and not self.doNotDescendForCaret(obj):
+                return self.findNextCaretInOrder(obj[0], -1)
+            elif offset < 0 and not self.isTextBlockElement(obj) and not self.hasNoSize(obj):
+                return obj, 0
+
+        # If we're here, start looking up the the tree, up to the document.
+        documentFrame = self.documentFrame()
+        if self.isSameObject(obj, documentFrame):
+            return None, -1
+
+        while obj.parent:
+            parent = obj.parent
+            if self.isZombie(parent):
+                replicant = self.findReplicant(self.documentFrame(), parent)
+                if replicant and not self.isZombie(replicant):
+                    parent = replicant
+                elif parent.parent:
+                    obj = parent
+                    continue
+                else:
+                    break
+
+            start, end, length = self._rangeInParentWithLength(obj)
+            if start + 1 == end and 0 <= start < end <= length:
+                return self.findNextCaretInOrder(parent, start)
+
+            index = obj.getIndexInParent() + 1
+            if 0 <= index < parent.childCount:
+                return self.findNextCaretInOrder(parent[index], -1)
+            obj = parent
+
+        return None, -1
+
+    def findPreviousCaretInOrder(self, obj=None, offset=-1):
+        if not obj:
+            obj, offset = self.getCaretContext()
+
+        if not obj or not self.inDocumentContent(obj):
+            return None, -1
+
+        if not (self.isHidden(obj) or self.isOffScreenLabel(obj) or self.isNonNavigablePopup(obj)):
+            text = self.queryNonEmptyText(obj)
+            if text:
+                allText = text.getText(0, -1)
+                if offset == -1 or offset > len(allText):
+                    offset = len(allText)
+                for i in range(offset - 1, -1, -1):
+                    child = self.getChildAtOffset(obj, i)
+                    if child and not self.isZombie(child):
+                        return self.findPreviousCaretInOrder(child, -1)
+                    if allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
+                        return obj, i
+            elif obj.childCount and not self.doNotDescendForCaret(obj):
+                return self.findPreviousCaretInOrder(obj[obj.childCount - 1], -1)
+            elif offset < 0 and not self.isTextBlockElement(obj) and not self.hasNoSize(obj):
+                return obj, 0
+
+        # If we're here, start looking up the the tree, up to the document.
+        documentFrame = self.documentFrame()
+        if self.isSameObject(obj, documentFrame):
+            return None, -1
+
+        while obj.parent:
+            parent = obj.parent
+            if self.isZombie(parent):
+                replicant = self.findReplicant(self.documentFrame(), parent)
+                if replicant and not self.isZombie(replicant):
+                    parent = replicant
+                elif parent.parent:
+                    obj = parent
+                    continue
+                else:
+                    break
+
+            start, end, length = self._rangeInParentWithLength(obj)
+            if start + 1 == end and 0 <= start < end <= length:
+                return self.findPreviousCaretInOrder(parent, start)
+
+            index = obj.getIndexInParent() - 1
+            if 0 <= index < parent.childCount:
+                return self.findPreviousCaretInOrder(parent[index], -1)
+            obj = parent
+
+        return None, -1
+
+    def handleAsLiveRegion(self, event):
+        if not _settingsManager.getSetting('inferLiveRegions'):
+            return False
+
+        return self.isLiveRegion(event.source)
+
+    def getPageSummary(self, obj):
+        docframe = self.documentFrame(obj)
+        col = docframe.queryCollection()
+        headings = 0
+        forms = 0
+        tables = 0
+        vlinks = 0
+        uvlinks = 0
+        percentRead = None
+
+        stateset = pyatspi.StateSet()
+        roles = [pyatspi.ROLE_HEADING, pyatspi.ROLE_LINK, pyatspi.ROLE_TABLE,
+                 pyatspi.ROLE_FORM]
+        rule = col.createMatchRule(stateset.raw(), col.MATCH_NONE,
+                                   "", col.MATCH_NONE,
+                                   roles, col.MATCH_ANY,
+                                   "", col.MATCH_NONE,
+                                   False)
+
+        matches = col.getMatches(rule, col.SORT_ORDER_CANONICAL, 0, True)
+        col.freeMatchRule(rule)
+        for obj in matches:
+            role = obj.getRole()
+            if role == pyatspi.ROLE_HEADING:
+                headings += 1
+            elif role == pyatspi.ROLE_FORM:
+                forms += 1
+            elif role == pyatspi.ROLE_TABLE and not self.isLayoutOnly(obj):
+                tables += 1
+            elif role == pyatspi.ROLE_LINK:
+                if obj.getState().contains(pyatspi.STATE_VISITED):
+                    vlinks += 1
+                else:
+                    uvlinks += 1
+
+        return [headings, forms, tables, vlinks, uvlinks, percentRead]
diff --git a/src/orca/scripts/web/speech_generator.py b/src/orca/scripts/toolkits/Gecko/speech_generator.py
similarity index 97%
rename from src/orca/scripts/web/speech_generator.py
rename to src/orca/scripts/toolkits/Gecko/speech_generator.py
index d2e232d..0e78cfa 100644
--- a/src/orca/scripts/web/speech_generator.py
+++ b/src/orca/scripts/toolkits/Gecko/speech_generator.py
@@ -335,6 +335,13 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
             result.append(text)
         return result
 
+    # TODO - JD: more crap to move to default and utilities....
+    def getAttribute(self, obj, attributeName):
+        attributes = obj.getAttributes()
+        for attribute in attributes:
+            if attribute.startswith(attributeName):
+                return attribute.split(":")[1]
+
     def _generatePositionInList(self, obj, **args):
         if _settingsManager.getSetting('onlySpeakDisplayedText'):
             return []
@@ -358,13 +365,8 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
         if self._script.utilities.isTextBlockElement(obj):
             return []
 
-        try:
-            attrs = dict([attr.split(':', 1) for attr in obj.getAttributes()])
-        except:
-            attrs = {}
-
-        position = attrs.get("posinset")
-        total = attrs.get("setsize")
+        position = self.getAttribute(obj, "posinset")
+        total = self.getAttribute(obj, "setsize")
         if position is None or total is None:
             return super()._generatePositionInList(obj, **args)
 
diff --git a/src/orca/scripts/web/tutorial_generator.py 
b/src/orca/scripts/toolkits/Gecko/tutorial_generator.py
similarity index 81%
rename from src/orca/scripts/web/tutorial_generator.py
rename to src/orca/scripts/toolkits/Gecko/tutorial_generator.py
index ef5605d..9f95072 100644
--- a/src/orca/scripts/web/tutorial_generator.py
+++ b/src/orca/scripts/toolkits/Gecko/tutorial_generator.py
@@ -23,13 +23,12 @@ __date__      = "$Date$"
 __copyright__ = "Copyright (c) 2014 Orca Team."
 __license__   = "LGPL"
 
-from orca import messages
-from orca import tutorialgenerator
+import orca.messages as messages
+import orca.tutorialgenerator as tutorial_generator
 
-
-class TutorialGenerator(tutorialgenerator.TutorialGenerator):
+class TutorialGenerator(tutorial_generator.TutorialGenerator):
     def __init__(self, script):
-        super().__init__(script)
+        tutorial_generator.TutorialGenerator.__init__(self, script)
 
     def _getFocusModeTutorial(self, obj, alreadyFocused, forceTutorial):
         binding = self._getBindingsForHandler("togglePresentationModeHandler")
@@ -43,4 +42,5 @@ class TutorialGenerator(tutorialgenerator.TutorialGenerator):
            and not self._script.useFocusMode(obj):
             return self._getFocusModeTutorial(obj, alreadyFocused, forceTutorial)
 
-        return super()._getModeTutorial(obj, alreadyFocused, forceTutorial)
+        return tutorial_generator.TutorialGenerator._getModeTutorial(
+            self, obj, alreadyFocused, forceTutorial)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]