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



commit 1f33c21269b2500c5e92c942a6f2524ae2599475
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Fri Jun 12 10:24:16 2015 -0400

    Create "web" script and use it for Gecko content

 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 ---
 src/orca/scripts/toolkits/Gecko/script.py          | 1628 +----------------
 .../scripts/toolkits/Gecko/script_utilities.py     | 1804 +-------------------
 src/orca/scripts/web/Makefile.am                   |   10 +
 src/orca/scripts/web/__init__.py                   |   26 +
 src/orca/scripts/web/bookmarks.py                  |  139 ++
 .../{toolkits/Gecko => web}/braille_generator.py   |   10 +-
 src/orca/scripts/web/script.py                     | 1529 ++++++++++++++++
 src/orca/scripts/web/script_utilities.py           | 1851 ++++++++++++++++++++
 .../{toolkits/Gecko => web}/speech_generator.py    |   16 +-
 .../{toolkits/Gecko => web}/tutorial_generator.py  |   12 +-
 19 files changed, 3680 insertions(+), 3627 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 58ca5c4..fdecc3a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -123,6 +123,7 @@ 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 7bc77f2..3ac2111 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -23,7 +23,6 @@ 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 4078c27..9536a2c 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.py" % moduleName)
+            debug.println(debug.LEVEL_FINE, "Looking for %s" % moduleName)
             try:
                 module = importlib.import_module(moduleName)
             except ImportError:
                 debug.println(
-                    debug.LEVEL_FINE, "Could not import %s.py" % moduleName)
+                    debug.LEVEL_FINE, "Could not import %s" % moduleName)
                 continue
             except OSError:
                 debug.examineProcesses()
 
-            debug.println(debug.LEVEL_FINE, "Found %s.py" % moduleName)
+            debug.println(debug.LEVEL_FINE, "Found %s" % moduleName)
             try:
                 if hasattr(module, 'getScript'):
                     script = module.getScript(app)
                 else:
                     script = module.Script(app)
-                debug.println(debug.LEVEL_FINE, "Loaded %s.py" % moduleName)
+                debug.println(debug.LEVEL_FINE, "Loaded %s" % moduleName)
                 break
             except:
                 debug.printException(debug.LEVEL_FINEST)
                 debug.println(
-                    debug.LEVEL_FINEST, "Could not load %s.py" % moduleName)
+                    debug.LEVEL_FINEST, "Could not load %s" % moduleName)
 
         return script
 
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index 69c11be..bfdfbb0 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -1599,8 +1599,7 @@ class Utilities:
 
         return rv
 
-    @staticmethod
-    def characterOffsetInParent(obj):
+    def characterOffsetInParent(self, 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 49b289a..bd5c6c3 100644
--- a/src/orca/scripts/Makefile.am
+++ b/src/orca/scripts/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = apps toolkits
+SUBDIRS = apps toolkits web
 
 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 2c7b262..372d7db 100644
--- a/src/orca/scripts/apps/Thunderbird/speech_generator.py
+++ b/src/orca/scripts/apps/Thunderbird/speech_generator.py
@@ -17,8 +17,7 @@
 # Free Software Foundation, Inc., Franklin Street, Fifth Floor,
 # Boston MA  02110-1301 USA.
 
-""" Custom script for Thunderbird 3.
-"""
+"""Custom script for Thunderbird"""
 
 __id__        = "$Id$"
 __version__   = "$Revision$"
@@ -28,24 +27,14 @@ __license__   = "LGPL"
 
 import pyatspi
 
-import orca.scripts.toolkits.Gecko as Gecko
+from orca.scripts.web import speech_generator
 
-from orca.orca_i18n import _
 
-########################################################################
-#                                                                      #
-# Custom SpeechGenerator for Thunderbird                               #
-#                                                                      #
-########################################################################
-
-class SpeechGenerator(Gecko.SpeechGenerator):
-    """Provides a speech generator specific to Thunderbird.
-    """
-
-    # pylint: disable-msg=W0142
+class SpeechGenerator(speech_generator.SpeechGenerator):
+    """Provides a speech generator specific to Thunderbird."""
 
     def __init__(self, script):
-        Gecko.SpeechGenerator.__init__(self, script)
+        super().__init__(script)
 
     def _generateColumnHeader(self, obj, **args):
         """Returns an array of strings (and possibly voice and audio
@@ -53,31 +42,7 @@ class SpeechGenerator(Gecko.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 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
+        return []
diff --git a/src/orca/scripts/toolkits/Gecko/Makefile.am b/src/orca/scripts/toolkits/Gecko/Makefile.am
index 0d76502..bf85b9b 100644
--- a/src/orca/scripts/toolkits/Gecko/Makefile.am
+++ b/src/orca/scripts/toolkits/Gecko/Makefile.am
@@ -1,11 +1,7 @@
 orca_python_PYTHON = \
        __init__.py \
-       bookmarks.py \
-       braille_generator.py \
        script.py \
-       script_utilities.py \
-       speech_generator.py \
-       tutorial_generator.py
+       script_utilities.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 4209fcd..a1ec518 100644
--- a/src/orca/scripts/toolkits/Gecko/__init__.py
+++ b/src/orca/scripts/toolkits/Gecko/__init__.py
@@ -1,5 +1,2 @@
 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/script.py b/src/orca/scripts/toolkits/Gecko/script.py
index 152ba89..07c8b32 100644
--- a/src/orca/scripts/toolkits/Gecko/script.py
+++ b/src/orca/scripts/toolkits/Gecko/script.py
@@ -19,17 +19,6 @@
 # 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$"
@@ -38,107 +27,22 @@ __copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." \
                 "Copyright (c) 2014-2015 Igalia, S.L."
 __license__   = "LGPL"
 
-from gi.repository import Gtk
 import pyatspi
-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
-
-_settingsManager = settings_manager.getManager()
-
-########################################################################
-#                                                                      #
-# Script                                                               #
-#                                                                      #
-########################################################################
+from orca import debug
+from orca import orca
+from orca.scripts import default
+from orca.scripts import web
+from .script_utilities import Utilities
 
-class Script(default.Script):
-    """The script for Firefox."""
 
-    ####################################################################
-    #                                                                  #
-    # Overridden Script Methods                                        #
-    #                                                                  #
-    ####################################################################
+class Script(web.Script):
 
     def __init__(self, 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
+        super().__init__(app)
 
-        # 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 = {}
+        # TODO - JD: This should also not be needed. In theory, they've
+        # converted to the new attribute styles.
 
         # A dictionary of Gecko-style attribute names and their equivalent/
         # expected names. This is necessary so that we can present the
@@ -172,574 +76,20 @@ class Script(default.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 getEnabledStructuralNavigationTypes(self):
-        """Returns a list of the structural navigation object types
-        enabled in this script.
-        """
-
-        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
-
-        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
+    def locusOfFocusChanged(self, event, oldFocus, newFocus):
+        """Handles changes of focus of interest to the script."""
 
-        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 = []
+        if super().locusOfFocusChanged(event, oldFocus, newFocus):
             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]
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.locusOfFocusChanged(self, event, oldFocus, newFocus)
 
     def onActiveChanged(self, event):
         """Callback for object:state-changed:active accessibility events."""
@@ -759,305 +109,72 @@ class Script(default.Script):
     def onBusyChanged(self, event):
         """Callback for object:state-changed:busy accessibility events."""
 
-        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
+        if super().onBusyChanged(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.onBusyChanged(self, event)
 
     def onCaretMoved(self, event):
         """Callback for object:text-caret-moved accessibility events."""
 
-        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)
+        if super().onCaretMoved(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
         debug.println(debug.LEVEL_INFO, msg)
-        super().onCaretMoved(event)
-        return False
+        default.Script.onCaretMoved(self, event)
 
     def onCheckedChanged(self, event):
         """Callback for object:state-changed:checked accessibility events."""
 
-        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
+        if super().onCheckedChanged(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.onCheckedChanged(self, event)
 
     def onChildrenChanged(self, event):
         """Callback for object:children-changed accessibility events."""
 
-        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
+        if super().onChildrenChanged(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.onChildrenChanged(self, event)
 
     def onDocumentLoadComplete(self, event):
         """Callback for document:load-complete accessibility events."""
 
-        msg = "INFO: Updating loading state and resetting live regions"
+        if super().onDocumentLoadComplete(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
         debug.println(debug.LEVEL_INFO, msg)
-        self._loadingDocumentContent = False
-        self.liveRegionManager.reset()
-        return True
+        default.Script.onDocumentLoadComplete(self, event)
 
     def onDocumentLoadStopped(self, event):
         """Callback for document:load-stopped accessibility events."""
 
-        msg = "INFO: Updating loading state"
+        if super().onDocumentLoadStopped(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
         debug.println(debug.LEVEL_INFO, msg)
-        self._loadingDocumentContent = False
-        return True
+        default.Script.onDocumentLoadStopped(self, event)
 
     def onDocumentReload(self, event):
         """Callback for document:reload accessibility events."""
 
-        msg = "INFO: Updating loading state"
+        if super().onDocumentReload(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
         debug.println(debug.LEVEL_INFO, msg)
-        self._loadingDocumentContent = True
-        return True
+        default.Script.onDocumentReload(self, event)
 
     def onFocus(self, event):
         """Callback for focus: accessibility events."""
@@ -1068,7 +185,7 @@ class Script(default.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
-        # during the remainder of the unstable (3.11) development cycle.
+        # in which object:state-changed:focused events are missing.
 
         role = event.source.getRole()
 
@@ -1096,259 +213,72 @@ class Script(default.Script):
     def onFocusedChanged(self, event):
         """Callback for object:state-changed:focused accessibility events."""
 
-        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
+        if super().onFocusedChanged(event):
+            return
 
-        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
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.onFocusedChanged(self, event)
 
     def onMouseButton(self, event):
         """Callback for mouse:button accessibility events."""
 
-        self._lastCommandWasCaretNav = False
-        self._lastCommandWasStructNav = False
-        self._lastCommandWasMouseButton = True
-        super().onMouseButton(event)
-        return False
+        if super().onMouseButton(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.onMouseButton(self, event)
 
     def onNameChanged(self, event):
         """Callback for object:property-change:accessible-name events."""
 
-        if self.utilities.eventIsStatusBarNoise(event):
-            msg = "INFO: Ignoring event believed to be status bar noise"
-            debug.println(debug.LEVEL_INFO, msg)
-            return True
-
-        if event.source.getRole() == pyatspi.ROLE_FRAME:
-            msg = "INFO: Flusing messages from live region manager"
-            debug.println(debug.LEVEL_INFO, msg)
-            self.liveRegionManager.flushMessages()
+        if super().onNameChanged(event):
+            return
 
-        return True
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.onNameChanged(self, event)
 
     def onShowingChanged(self, event):
         """Callback for object:state-changed:showing accessibility events."""
 
-        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
+        if super().onShowingChanged(event):
+            return
 
-        return True
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.onShowingChanged(self, event)
 
     def onTextDeleted(self, event):
         """Callback for object:text-changed:delete accessibility events."""
 
-        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
-
-        if self.utilities.textEventIsDueToInsertion(event):
-            msg = "INFO: Ignoring event believed to be due to text insertion"
-            debug.println(debug.LEVEL_INFO, msg)
-            return True
+        if super().onTextDeleted(event):
+            return
 
-        msg = "INFO: Clearing content cache due to text deletion"
+        msg = "GECKO: Passing along event to default script"
         debug.println(debug.LEVEL_INFO, msg)
-        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
+        default.Script.onTextDeleted(self, event)
 
     def onTextInserted(self, event):
         """Callback for object:text-changed:insert accessibility events."""
 
-        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"
+        if super().onTextInserted(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
         debug.println(debug.LEVEL_INFO, msg)
-        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
+        default.Script.onTextInserted(self, event)
 
     def onTextSelectionChanged(self, event):
         """Callback for object:text-selection-changed accessibility events."""
 
-        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
+        if super().onTextSelectionChanged(event):
+            return
+
+        msg = "GECKO: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg)
+        default.Script.onTextSelectionChanged(self, event)
 
     def handleProgressBarUpdate(self, event, obj):
         """Determine whether this progress bar event should be spoken or not.
@@ -1367,381 +297,3 @@ class Script(default.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 50a3124..c5d5d8f 100644
--- a/src/orca/scripts/toolkits/Gecko/script_utilities.py
+++ b/src/orca/scripts/toolkits/Gecko/script_utilities.py
@@ -31,265 +31,17 @@ __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 import script_utilities
-from orca import settings
-from orca import settings_manager
+from orca.scripts import web
 
-_settingsManager = settings_manager.getManager()
 
-
-class Utilities(script_utilities.Utilities):
+class Utilities(web.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
@@ -300,32 +52,8 @@ class Utilities(script_utilities.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) """
 
@@ -459,1529 +187,3 @@ class Utilities(script_utilities.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/Makefile.am b/src/orca/scripts/web/Makefile.am
new file mode 100644
index 0000000..e74d1d7
--- /dev/null
+++ b/src/orca/scripts/web/Makefile.am
@@ -0,0 +1,10 @@
+orca_python_PYTHON = \
+       __init__.py \
+       bookmarks.py \
+       braille_generator.py \
+       script.py \
+       script_utilities.py \
+       speech_generator.py \
+       tutorial_generator.py
+
+orca_pythondir=$(pkgpythondir)/scripts/web
diff --git a/src/orca/scripts/web/__init__.py b/src/orca/scripts/web/__init__.py
new file mode 100644
index 0000000..786b92f
--- /dev/null
+++ b/src/orca/scripts/web/__init__.py
@@ -0,0 +1,26 @@
+# Orca
+#
+# Copyright 2005-2009 Sun Microsystems Inc.
+# Copyright 2010 Orca Team.
+# Copyright 2014-2015 Igalia, S.L.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+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/web/bookmarks.py b/src/orca/scripts/web/bookmarks.py
new file mode 100644
index 0000000..ac1998a
--- /dev/null
+++ b/src/orca/scripts/web/bookmarks.py
@@ -0,0 +1,139 @@
+# 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.
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc."
+__license__   = "LGPL"
+
+import pyatspi
+
+from orca import bookmarks
+from orca import messages
+
+
+class Bookmarks(bookmarks.Bookmarks):
+
+    def __init__(self, script):
+        super().__init__(script)
+        self._currentbookmarkindex = {}
+
+    def addBookmark(self, inputEvent):
+        """Add an in-page accessible object bookmark for this key and URI."""
+
+        index = (inputEvent.hw_code, self.getURIKey())
+        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, offset = self._bookmarks[index]
+        except KeyError:
+            self._script.systemBeep()
+            return
+
+        obj = self.pathToObj(path)
+        if not obj:
+            self._script.systemBeep()
+            return
+
+        self._script.utilities.setCaretPosition(obj, offset)
+        contents = self._script.utilities.getObjectContentsAtOffset(obj, offset)
+        self._script.speakContents(contents)
+        self._script.displayContents(contents)
+        self._currentbookmarkindex[index[1]] = index[0]
+
+    def saveBookmarks(self, inputEvent):
+        """Save the bookmarks for this script."""
+
+        saved = {}
+        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)
+
+        for o in self._saveObservers:
+            o()
+
+    def goToNextBookmark(self, inputEvent):
+        """Go to the next bookmark location."""
+
+        # 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()
+
+        if len(thispage_hwkeys) == 0:
+            self._script.systemBeep()
+            return
+
+        if len(thispage_hwkeys) == 1 or current_uri not in self._currentbookmarkindex:
+            self.goToBookmark(None, index=(thispage_hwkeys[0], current_uri))
+            return
+
+        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."""
+
+        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()
+
+        if len(thispage_hwkeys) == 0:
+            self._script.systemBeep()
+            return
+
+        if len(thispage_hwkeys) == 1 or current_uri not in self._currentbookmarkindex:
+            self.goToBookmark(None, index=(thispage_hwkeys[0], current_uri))
+            return
+
+        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/toolkits/Gecko/braille_generator.py b/src/orca/scripts/web/braille_generator.py
similarity index 95%
rename from src/orca/scripts/toolkits/Gecko/braille_generator.py
rename to src/orca/scripts/web/braille_generator.py
index 9d3ab63..74b176e 100644
--- a/src/orca/scripts/toolkits/Gecko/braille_generator.py
+++ b/src/orca/scripts/web/braille_generator.py
@@ -35,6 +35,7 @@ from orca import messages
 from orca import object_properties
 from orca import orca_state
 
+
 class BrailleGenerator(braille_generator.BrailleGenerator):
 
     def __init__(self, script):
@@ -108,6 +109,7 @@ 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):
@@ -121,20 +123,16 @@ 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/web/script.py b/src/orca/scripts/web/script.py
new file mode 100644
index 0000000..dcba33b
--- /dev/null
+++ b/src/orca/scripts/web/script.py
@@ -0,0 +1,1529 @@
+# Orca
+#
+# Copyright 2005-2009 Sun Microsystems Inc.
+# Copyright 2010 Orca Team.
+# Copyright 2014-2015 Igalia, S.L.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2005-2009 Sun Microsystems Inc." \
+                "Copyright (c) 2010 Orca Team." \
+                "Copyright (c) 2014-2015 Igalia, S.L."
+__license__   = "LGPL"
+
+from gi.repository import Gtk
+import pyatspi
+import time
+
+from orca import caret_navigation
+from orca import cmdnames
+from orca import keybindings
+from orca import debug
+from orca import eventsynthesizer
+from orca import guilabels
+from orca import input_event
+from orca import liveregions
+from orca import messages
+from orca import object_properties
+from orca import orca
+from orca import orca_state
+from orca import settings
+from orca import settings_manager
+from orca import speech
+from orca import speechserver
+from orca import structural_navigation
+from orca.acss import ACSS
+from orca.scripts import default
+
+from .bookmarks import Bookmarks
+from .braille_generator import BrailleGenerator
+from .speech_generator import SpeechGenerator
+from .tutorial_generator import TutorialGenerator
+from .script_utilities import Utilities
+
+_settingsManager = settings_manager.getManager()
+
+
+class Script(default.Script):
+
+    def __init__(self, app):
+        super().__init__(app)
+
+        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._inFocusMode = False
+        self._focusModeIsSticky = False
+
+        if _settingsManager.getSetting('caretNavigationEnabled') == None:
+            _settingsManager.setSetting('caretNavigationEnabled', True)
+        if _settingsManager.getSetting('sayAllOnLoad') == None:
+            _settingsManager.setSetting('sayAllOnLoad', True)
+
+        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
+
+    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 getAppKeyBindings(self):
+        """Returns the application-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 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 getBookmarks(self):
+        """Returns the "bookmarks" class for this script."""
+
+        try:
+            return self.bookmarks
+        except AttributeError:
+            self.bookmarks = Bookmarks(self)
+            return self.bookmarks
+
+    def getBrailleGenerator(self):
+        """Returns the braille generator for this script."""
+
+        return BrailleGenerator(self)
+
+    def getCaretNavigation(self):
+        """Returns the caret navigation support for this script."""
+
+        return caret_navigation.CaretNavigation(self)
+
+    def getEnabledStructuralNavigationTypes(self):
+        """Returns the structural navigation object types for this script."""
+
+        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 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 getAppPreferencesGUI(self):
+        """Return a GtkGrid containing app-unique configuration items."""
+
+        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 skipObjectEvent(self, event):
+        """Returns True if this object event should be skipped."""
+
+        if event.type.startswith('object:state-changed:focused') \
+           and event.detail1:
+            if event.source.getRole() == pyatspi.ROLE_LINK:
+                return False
+
+        return super().skipObjectEvent(event)
+
+    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
+
+        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 super().sayAll(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 super()._rewindSayAll(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 super()._fastForwardSayAll(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:
+            super().__sayAllProgressCallback(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 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 speakContents(self, contents):
+        """Speaks the specified contents."""
+
+        utterances = self.speechGenerator.generateContents(contents)
+        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 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 panBrailleLeft(self, inputEvent=None, panAmount=0):
+        """Pans braille to the left."""
+
+        if self.flatReviewContext \
+           or not self.utilities.inDocumentContent() \
+           or not self.isBrailleBeginningShowing():
+            super().panBrailleLeft(inputEvent, panAmount)
+            return
+
+        contents = self.utilities.getPreviousLineContents()
+        if not contents:
+            return
+
+        obj, start, end, string = contents[0]
+        self.utilities.setCaretPosition(obj, start)
+
+        # Hack: When panning to the left in a document, we want to start at
+        # the right/bottom of each new object. For now, we'll pan there.
+        # When time permits, we'll give our braille code some smarts.
+        while self.panBrailleInDirection(panToLeft=False):
+            pass
+
+        self.refreshBraille(False)
+        return True
+
+    def panBrailleRight(self, inputEvent=None, panAmount=0):
+        """Pans braille to the right."""
+
+        if self.flatReviewContext \
+           or not self.utilities.inDocumentContent() \
+           or not self.isBrailleEndShowing():
+            super().panBrailleRight(inputEvent, panAmount)
+            return
+
+        contents = self.utilities.getNextLineContents()
+        if not contents:
+            return
+
+        obj, start, end, string = contents[0]
+        self.utilities.setCaretPosition(obj, start)
+        self.updateBraille(obj)
+
+        # Hack: When panning to the right in a document, we want to start at
+        # the left/top of each new object. For now, we'll pan there. When time
+        # permits, we'll give our braille code some smarts.
+        while self.panBrailleInDirection(panToLeft=True):
+            pass
+
+        self.refreshBraille(False)
+        return True
+
+    def useCaretNavigationModel(self, keyboardEvent):
+        """Returns True if caret navigation should be used."""
+
+        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 structural navigation should be used."""
+
+        if not self.structuralNavigation.enabled or self._inFocusMode:
+            return False
+
+        if not self.utilities.inDocumentContent():
+            return False
+
+        return True
+ 
+    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
+
+    def moveToMouseOver(self, inputEvent):
+        """Moves the context to/from the mouseover which has just appeared."""
+
+        if not self._lastMouseOverObject:
+            self.presentMessage(messages.MOUSE_OVER_NOT_FOUND)
+            return
+
+        if self._inMouseOverObject:
+            x, y = self.oldMouseCoordinates
+            eventsynthesizer.routeToPoint(x, y)
+            self.restorePreMouseOverContext()
+            return
+
+        obj = self._lastMouseOverObject
+        obj, offset = self.utilities.findFirstCaretContext(obj, 0)
+        if not obj:
+            return
+
+        if obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+            obj.queryComponent().grabFocus()
+
+        contents = self.utilities.getObjectContentsAtOffset(obj, offset)
+        self.utilities.setCaretPosition(obj, offset)
+        self.speakContents(contents)
+        self.updateBraille(obj)
+        self._inMouseOverObject = True
+
+    def restorePreMouseOverContext(self):
+        """Cleans things up after a mouse-over object has been hidden."""
+
+        obj, offset = self._preMouseOverContext
+        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 locusOfFocusChanged(self, event, oldFocus, newFocus):
+        """Handles changes of focus of interest to the script."""
+
+        if newFocus and self.utilities.isZombie(newFocus):
+            msg = "WEB: New focus is Zombie" % newFocus
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(newFocus):
+            msg = "WEB: Locus of focus changed to non-document obj"
+            self._madeFindAnnouncement = False
+            self._inFocusMode = False
+            debug.println(debug.LEVEL_INFO, msg)
+            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 onBusyChanged(self, event):
+        """Callback for object:state-changed:busy accessibility events."""
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        if not self.utilities.inDocumentContent(orca_state.locusOfFocus):
+            msg = "WEB: 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 = "WEB: Could not get caret context"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.isFocusModeWidget(obj):
+            msg = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: Doing SayAll"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.sayAll(None)
+        else:
+            msg = "WEB: 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 self.utilities.isZombie(event.source):
+            msg = "WEB: Event source is Zombie"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        if self._lastCommandWasCaretNav:
+            msg = "WEB: Event ignored: Last command was caret nav"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self._lastCommandWasStructNav:
+            msg = "WEB: Event ignored: Last command was struct nav"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self._lastCommandWasMouseButton:
+            msg = "WEB: 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 = "WEB: 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 = "WEB: Event ignored: Autocomplete noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.textEventIsForNonNavigableTextObject(event):
+            msg = "WEB: Event ignored: Event source is non-navigable text object"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.textEventIsDueToInsertion(event):
+            msg = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: Event ignored: Was for embedded non-textblock"
+                debug.println(debug.LEVEL_INFO, msg)
+                return True
+
+            msg = "WEB: 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 = "WEB: Setting locusOfFocus, context to: %s, %i" % \
+                  (event.source, event.detail1)
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        self.utilities.setCaretContext(obj, offset)
+        msg = "WEB: Setting context to: %s, %i" % (obj, offset)
+        debug.println(debug.LEVEL_INFO, msg)
+        return False
+
+    def onCheckedChanged(self, event):
+        """Callback for object:state-changed:checked accessibility events."""
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        obj, offset = self.utilities.getCaretContext()
+        if obj != event.source:
+            msg = "WEB: 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 = "WEB: 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 = "WEB: Event is something default can handle"
+            debug.println(debug.LEVEL_INFO, msg)
+            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 self.utilities.handleAsLiveRegion(event):
+            msg = "WEB: Event to be handled as live region"
+            debug.println(debug.LEVEL_INFO, msg)
+            self.liveRegionManager.handleEvent(event)
+            return True
+
+        if self._loadingDocumentContent:
+            msg = "WEB: 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 = "WEB: Ignoring because any data is null or zombified."
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            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 = "WEB: 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 = "WEB: 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
+
+        return False
+
+    def onDocumentLoadComplete(self, event):
+        """Callback for document:load-complete accessibility events."""
+
+        msg = "WEB: Updating loading state and resetting live regions"
+        debug.println(debug.LEVEL_INFO, msg)
+        self._loadingDocumentContent = False
+        self.liveRegionManager.reset()
+        return True
+
+    def onDocumentLoadStopped(self, event):
+        """Callback for document:load-stopped accessibility events."""
+
+        msg = "WEB: Updating loading state"
+        debug.println(debug.LEVEL_INFO, msg)
+        self._loadingDocumentContent = False
+        return True
+
+    def onDocumentReload(self, event):
+        """Callback for document:reload accessibility events."""
+
+        msg = "WEB: Updating loading state"
+        debug.println(debug.LEVEL_INFO, msg)
+        self._loadingDocumentContent = True
+        return True
+
+    def onFocusedChanged(self, event):
+        """Callback for object:state-changed:focused accessibility events."""
+
+        if not event.detail1:
+            msg = "WEB: Ignoring because event source lost focus"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.isZombie(event.source):
+            msg = "WEB: Event source is Zombie"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        state = event.source.getState()
+        if state.contains(pyatspi.STATE_EDITABLE):
+            msg = "WEB: Event source is editable"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        role = event.source.getRole()
+        if role in [pyatspi.ROLE_DIALOG, pyatspi.ROLE_ALERT]:
+            msg = "WEB: Event handled: Setting locusOfFocus to event source"
+            debug.println(debug.LEVEL_INFO, msg)
+            orca.setLocusOfFocus(event, event.source)
+            return True
+
+        if self._lastCommandWasCaretNav:
+            msg = "WEB: Event ignored: Last command was caret nav"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self._lastCommandWasStructNav:
+            msg = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: Event ignored: Source is not focusable or focused"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        return False
+
+    def onMouseButton(self, event):
+        """Callback for mouse:button accessibility events."""
+
+        self._lastCommandWasCaretNav = False
+        self._lastCommandWasStructNav = False
+        self._lastCommandWasMouseButton = True
+        return False
+
+    def onNameChanged(self, event):
+        """Callback for object:property-change:accessible-name events."""
+
+        if self.utilities.eventIsStatusBarNoise(event):
+            msg = "WEB: Ignoring event believed to be status bar noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if event.source.getRole() == pyatspi.ROLE_FRAME:
+            msg = "WEB: 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 not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        return True
+
+    def onTextDeleted(self, event):
+        """Callback for object:text-changed:delete accessibility events."""
+
+        if self.utilities.eventIsStatusBarNoise(event):
+            msg = "WEB: Ignoring event believed to be status bar noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        if self.utilities.eventIsAutocompleteNoise(event):
+            msg = "WEB: Ignoring event believed to be autocomplete noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.textEventIsDueToInsertion(event):
+            msg = "WEB: Ignoring event believed to be due to text insertion"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        msg = "WEB: Clearing content cache due to text deletion"
+        debug.println(debug.LEVEL_INFO, msg)
+        self.utilities.clearContentCache()
+
+        state = event.source.getState()
+        if not state.contains(pyatspi.STATE_EDITABLE):
+            if self.inMouseOverObject \
+               and self.utilities.isZombie(self.lastMouseOverObject):
+                msg = "WEB: Restoring pre-mouseover context"
+                debug.println(debug.LEVEL_INFO, msg)
+                self.restorePreMouseOverContext()
+
+            msg = "WEB: Done processing non-editable source"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        return False
+
+    def onTextInserted(self, event):
+        """Callback for object:text-changed:insert accessibility events."""
+
+        if self.utilities.eventIsStatusBarNoise(event):
+            msg = "WEB: Ignoring event believed to be status bar noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        if self.utilities.eventIsAutocompleteNoise(event):
+            msg = "WEB: 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 = "WEB: Ignoring: Event was for embedded object char"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        msg = "WEB: Clearing content cache due to text insertion"
+        debug.println(debug.LEVEL_INFO, msg)
+        self.utilities.clearContentCache()
+
+        if self.utilities.handleAsLiveRegion(event):
+            msg = "WEB: 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 = "WEB: 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 = "WEB: Done processing non-editable, non-locusOfFocus source"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        return False
+
+    def onTextSelectionChanged(self, event):
+        """Callback for object:text-selection-changed accessibility events."""
+
+        if self.utilities.isZombie(event.source):
+            msg = "WEB: Event source is Zombie"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if not self.utilities.inDocumentContent(event.source):
+            msg = "WEB: Event source is not in document content"
+            debug.println(debug.LEVEL_INFO, msg)
+            return False
+
+        if self.utilities.inFindToolbar():
+            msg = "WEB: 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 = "WEB: Ignoring: Event in document content; focus is not"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.eventIsAutocompleteNoise(event):
+            msg = "WEB: Ignoring: Event believed to be autocomplete noise"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        if self.utilities.textEventIsForNonNavigableTextObject(event):
+            msg = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: Ignoring: Source is context ancestor"
+            debug.println(debug.LEVEL_INFO, msg)
+            return True
+
+        return False
diff --git a/src/orca/scripts/web/script_utilities.py b/src/orca/scripts/web/script_utilities.py
new file mode 100644
index 0000000..2d88c95
--- /dev/null
+++ b/src/orca/scripts/web/script_utilities.py
@@ -0,0 +1,1851 @@
+# Orca
+#
+# Copyright 2010 Joanmarie Diggs.
+# Copyright 2014-2015 Igalia, S.L.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2010 Joanmarie Diggs." \
+                "Copyright (c) 2014-2015 Igalia, S.L."
+__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 import script_utilities
+from orca import settings
+from orca import settings_manager
+
+_settingsManager = settings_manager.getManager()
+
+
+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, "WEB: 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
+
+        if obj and obj.parent \
+           and obj.parent.getRole() == pyatspi.ROLE_AUTOCOMPLETE:
+            return False
+
+        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 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 = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: 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 NotImplementedError:
+            msg = "WEB: %s does not implement the hyperlink interface" % obj
+            debug.println(debug.LEVEL_INFO, msg)
+            return -1, -1
+        except:
+            msg = "WEB: Exception getting hyperlink indices for %s" % obj
+            debug.println(debug.LEVEL_INFO, msg)
+            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 = "WEB: 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 = "WEB: 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 = "WEB: First caret context for %s, %i is %s, %i" % (obj, offset, nextObj, 
nextOffset)
+                    debug.println(debug.LEVEL_INFO, msg)
+                    return nextObj, nextOffset
+
+            msg = "WEB: 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 = "WEB: 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 = "WEB: 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 = "WEB: 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/toolkits/Gecko/speech_generator.py b/src/orca/scripts/web/speech_generator.py
similarity index 97%
rename from src/orca/scripts/toolkits/Gecko/speech_generator.py
rename to src/orca/scripts/web/speech_generator.py
index 0e78cfa..d2e232d 100644
--- a/src/orca/scripts/toolkits/Gecko/speech_generator.py
+++ b/src/orca/scripts/web/speech_generator.py
@@ -335,13 +335,6 @@ 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 []
@@ -365,8 +358,13 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
         if self._script.utilities.isTextBlockElement(obj):
             return []
 
-        position = self.getAttribute(obj, "posinset")
-        total = self.getAttribute(obj, "setsize")
+        try:
+            attrs = dict([attr.split(':', 1) for attr in obj.getAttributes()])
+        except:
+            attrs = {}
+
+        position = attrs.get("posinset")
+        total = attrs.get("setsize")
         if position is None or total is None:
             return super()._generatePositionInList(obj, **args)
 
diff --git a/src/orca/scripts/toolkits/Gecko/tutorial_generator.py 
b/src/orca/scripts/web/tutorial_generator.py
similarity index 81%
rename from src/orca/scripts/toolkits/Gecko/tutorial_generator.py
rename to src/orca/scripts/web/tutorial_generator.py
index 9f95072..ef5605d 100644
--- a/src/orca/scripts/toolkits/Gecko/tutorial_generator.py
+++ b/src/orca/scripts/web/tutorial_generator.py
@@ -23,12 +23,13 @@ __date__      = "$Date$"
 __copyright__ = "Copyright (c) 2014 Orca Team."
 __license__   = "LGPL"
 
-import orca.messages as messages
-import orca.tutorialgenerator as tutorial_generator
+from orca import messages
+from orca import tutorialgenerator
 
-class TutorialGenerator(tutorial_generator.TutorialGenerator):
+
+class TutorialGenerator(tutorialgenerator.TutorialGenerator):
     def __init__(self, script):
-        tutorial_generator.TutorialGenerator.__init__(self, script)
+        super().__init__(script)
 
     def _getFocusModeTutorial(self, obj, alreadyFocused, forceTutorial):
         binding = self._getBindingsForHandler("togglePresentationModeHandler")
@@ -42,5 +43,4 @@ class TutorialGenerator(tutorial_generator.TutorialGenerator):
            and not self._script.useFocusMode(obj):
             return self._getFocusModeTutorial(obj, alreadyFocused, forceTutorial)
 
-        return tutorial_generator.TutorialGenerator._getModeTutorial(
-            self, obj, alreadyFocused, forceTutorial)
+        return super()._getModeTutorial(obj, alreadyFocused, forceTutorial)


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