[orca] Create "web" script and use it for Gecko content
- From: Joanmarie Diggs <joanied src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [orca] Create "web" script and use it for Gecko content
- Date: Fri, 12 Jun 2015 14:34:36 +0000 (UTC)
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]