# Orca
#
# Copyright 2005-2009 Sun Microsystems Inc.
# Copyright 2010 Orca Team.
#
# 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.
# [[[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
"""Custom script for Gecko toolkit.
Please refer to the following URL for more information on the AT-SPI
implementation in Gecko:
http://developer.mozilla.org/en/docs/Accessibility/ATSPI_Support
"""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2010 Orca Team."
__license__ = "LGPL"
import atk
import gtk
import pyatspi
import re
import time
import urlparse
import orca.braille as braille
import orca.debug as debug
import orca.scripts.default as default
import orca.eventsynthesizer as eventsynthesizer
import orca.input_event as input_event
import orca.keybindings as keybindings
import orca.liveregions as liveregions
try:
import orca.gsmag as mag
except:
import orca.mag as mag
import orca.orca as orca
import orca.orca_state as orca_state
import orca.rolenames as rolenames
import orca.settings as settings
import orca.speech as speech
import orca.speechserver as speechserver
import keymaps
import script_settings
from braille_generator import BrailleGenerator
from speech_generator import SpeechGenerator
from formatting import Formatting
from bookmarks import GeckoBookmarks
from structural_navigation import GeckoStructuralNavigation
from script_utilities import Utilities
from orca.orca_i18n import _
from orca.speech_generator import Pause
from orca.acss import ACSS
_settingsManager = getattr(orca, '_settingsManager')
########################################################################
# #
# Script #
# #
########################################################################
class Script(default.Script):
"""The script for Firefox."""
####################################################################
# #
# Overridden Script Methods #
# #
####################################################################
def __init__(self, app):
default.Script.__init__(self, app)
# Initialize variables to make pylint happy.
#
self.arrowToLineBeginningCheckButton = None
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.grabFocusOnAncestorCheckButton = None
# _caretNavigationFunctions are functions that represent fundamental
# ways to move the caret (e.g., by the arrow keys).
#
self._caretNavigationFunctions = \
[Script.goNextCharacter,
Script.goPreviousCharacter,
Script.goNextWord,
Script.goPreviousWord,
Script.goNextLine,
Script.goPreviousLine,
Script.expandComboBox,
Script.goTopOfFile,
Script.goBottomOfFile,
Script.goBeginningOfLine,
Script.goEndOfLine]
self._liveRegionFunctions = \
[Script.setLivePolitenessOff,
Script.advanceLivePoliteness,
Script.monitorLiveRegions,
Script.reviewLiveAnnouncement]
if script_settings.controlCaretNavigation:
debug.println(debug.LEVEL_CONFIGURATION,
"Orca is controlling the caret.")
else:
debug.println(debug.LEVEL_CONFIGURATION,
"Gecko is controlling the caret.")
# We keep track of whether we're currently in the process of
# loading a page.
#
self._loadingDocumentContent = False
self._loadingDocumentTime = 0.0
# 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
# We need to be able to distinguish focus events that are triggered
# by the call to grabFocus() in setCaretPosition() from those that
# are valid. See bug #471537.
#
self._objectForFocusGrab = None
# We don't want to prevent the user from arrowing into an
# autocomplete when it appears in a search form. We need to
# keep track if one has appeared or disappeared.
#
self._autocompleteVisible = False
# Create the live region manager and start the message manager
self.liveMngr = liveregions.LiveRegionManager(self)
# We want to keep track of the line contents we just got so that
# we can speak and braille this information without having to call
# getLineContentsAtOffset() twice.
#
self._previousLineContents = None
self.currentLineContents = None
self._nextLineContents = None
# guessTheLabel() is an expensive method. If we cache the guessed
# labels, we'll see a performance improvement when a form field
# is Tab/Shift+Tab'ed/Arrowed back to. In addition, we can check
# for non-label labels when looking at line content.
#
self._guessedLabels = {}
# 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 = {}
# Last focused frame. We are only interested in frame focused events
# when it is a different frame, so here we store the last frame
# that recieved state-changed:focused.
#
self._currentFrame = None
# All of the text attributes available for presentation to the
# user. This is necessary because the attributes used by Gecko
# are different from the default set.
#
self.allTextAttributes = \
"background-color:; color:; font-family:; font-size:; " \
"font-style:normal; font-weight:400; language:none; " \
"text-line-through-style:; text-align:start; text-indent:0px; " \
"text-underline-style:; text-position:baseline; " \
"invalid:none; writing-mode:lr;"
# The default set of text attributes to present to the user. This
# is necessary because the attributes used by Gecko are different
# from the default set.
#
self.enabledBrailledTextAttributes = \
"font-size:; font-family:; font-weight:400; text-indent:0px; " \
"text-underline-style:none; text-align:start; " \
"text-line-through-style:none; font-style:normal; invalid:none;"
self.enabledSpokenTextAttributes = \
"font-size:; font-family:; font-weight:400; text-indent:0px; " \
"text-underline-style:none; text-align:start; " \
"text-line-through-style:none; font-style:normal; invalid:none;"
# A dictionary of Gecko-style attribute names and their equivalent/
# expected names. This is necessary so that we can present the
# attributes to the user in a consistent fashion across apps and
# toolkits. Note that underlinesolid and line-throughsolid are
# temporary fixes: text_attribute_names.py assumes a one-to-one
# correspondence. This is not a problem when going from attribute
# name to localized name; in the reverse direction, we need more
# context (i.e. we can't safely make them both be "solid"). A
# similar issue exists with "start" which means no justification
# has explicitly been set. If we set that to "none", "none" will
# no longer have a single reverse translation.
#
self.attributeNamesDict = {
"font-weight" : "weight",
"font-family" : "family-name",
"font-style" : "style",
"text-align" : "justification",
"text-indent" : "indent",
"font-size" : "size",
"background-color" : "bg-color",
"color" : "fg-color",
"text-line-through-style" : "strikethrough",
"text-underline-style" : "underline",
"text-position" : "vertical-align",
"writing-mode" : "direction",
"-moz-left" : "left",
"-moz-right" : "right",
"-moz-center" : "center",
"start" : "no justification",
"underlinesolid" : "single",
"line-throughsolid" : "solid"}
# We need to save our special attributes so that we can revert to
# the default text attributes when giving up focus to another app
# and restore them upon return.
#
self.savedEnabledBrailledTextAttributes = None
self.savedEnabledSpokenTextAttributes = None
self.savedAllTextAttributes = None
# 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
def activate(self):
"""Called when this script is activated."""
self.savedEnabledBrailledTextAttributes = \
_settingsManager.getSetting('enabledBrailledTextAttributes')
_settingsManager.setSetting(
'enabledBrailledTextAttributes', self.enabledBrailledTextAttributes)
self.savedEnabledSpokenTextAttributes = \
_settingsManager.getSetting('enabledSpokenTextAttributes')
_settingsManager.setSetting(
'enabledSpokenTextAttributes', self.enabledSpokenTextAttributes)
self.savedAllTextAttributes = \
_settingsManager.getSetting('allTextAttributes')
_settingsManager.setSetting('allTextAttributes', self.allTextAttributes)
default.Script.activate(self)
def deactivate(self):
"""Called when this script is deactivated."""
_settingsManager.setSetting('enabledBrailledTextAttributes',
self.savedEnabledBrailledTextAttributes)
_settingsManager.setSetting('enabledSpokenTextAttributes',
self.savedEnabledSpokenTextAttributes)
_settingsManager.setSetting('allTextAttributes',
self.savedAllTextAttributes)
default.Script.deactivate(self)
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 getFormatting(self):
"""Returns the formatting strings for this script."""
return Formatting(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.
"""
enabledTypes = [GeckoStructuralNavigation.ANCHOR,
GeckoStructuralNavigation.BLOCKQUOTE,
GeckoStructuralNavigation.BUTTON,
GeckoStructuralNavigation.CHECK_BOX,
GeckoStructuralNavigation.CHUNK,
GeckoStructuralNavigation.COMBO_BOX,
GeckoStructuralNavigation.ENTRY,
GeckoStructuralNavigation.FORM_FIELD,
GeckoStructuralNavigation.HEADING,
GeckoStructuralNavigation.LANDMARK,
GeckoStructuralNavigation.LIST,
GeckoStructuralNavigation.LIST_ITEM,
GeckoStructuralNavigation.LIVE_REGION,
GeckoStructuralNavigation.PARAGRAPH,
GeckoStructuralNavigation.RADIO_BUTTON,
GeckoStructuralNavigation.SEPARATOR,
GeckoStructuralNavigation.TABLE,
GeckoStructuralNavigation.TABLE_CELL,
GeckoStructuralNavigation.UNVISITED_LINK,
GeckoStructuralNavigation.VISITED_LINK]
return enabledTypes
def getStructuralNavigation(self):
"""Returns the 'structural navigation' class for this script.
"""
types = self.getEnabledStructuralNavigationTypes()
enable = script_settings.structuralNavigationEnabled
return GeckoStructuralNavigation(self, types, enable)
def setupInputEventHandlers(self):
"""Defines InputEventHandler fields for this script that can be
called by the key and braille bindings.
"""
default.Script.setupInputEventHandlers(self)
self.inputEventHandlers.update(\
self.structuralNavigation.inputEventHandlers)
# Debug only.
#
self.inputEventHandlers["dumpContentsHandler"] = \
input_event.InputEventHandler(
Script.dumpContents,
"Dumps document content to stdout.")
self.inputEventHandlers["goNextCharacterHandler"] = \
input_event.InputEventHandler(
Script.goNextCharacter,
# Translators: this is for navigating HTML content one
# character at a time.
#
_("Goes to next character."))
self.inputEventHandlers["goPreviousCharacterHandler"] = \
input_event.InputEventHandler(
Script.goPreviousCharacter,
# Translators: this is for navigating HTML content one
# character at a time.
#
_( "Goes to previous character."))
self.inputEventHandlers["goNextWordHandler"] = \
input_event.InputEventHandler(
Script.goNextWord,
# Translators: this is for navigating HTML content one
# word at a time.
#
_("Goes to next word."))
self.inputEventHandlers["goPreviousWordHandler"] = \
input_event.InputEventHandler(
Script.goPreviousWord,
# Translators: this is for navigating HTML content one
# word at a time.
#
_("Goes to previous word."))
self.inputEventHandlers["goNextLineHandler"] = \
input_event.InputEventHandler(
Script.goNextLine,
# Translators: this is for navigating HTML content one
# line at a time.
#
_("Goes to next line."))
self.inputEventHandlers["goPreviousLineHandler"] = \
input_event.InputEventHandler(
Script.goPreviousLine,
# Translators: this is for navigating HTML content one
# line at a time.
#
_("Goes to previous line."))
self.inputEventHandlers["goTopOfFileHandler"] = \
input_event.InputEventHandler(
Script.goTopOfFile,
# Translators: this command will move the user to the
# beginning of an HTML document.
#
_("Goes to the top of the file."))
self.inputEventHandlers["goBottomOfFileHandler"] = \
input_event.InputEventHandler(
Script.goBottomOfFile,
# Translators: this command will move the user to the
# end of an HTML document.
#
_("Goes to the bottom of the file."))
self.inputEventHandlers["goBeginningOfLineHandler"] = \
input_event.InputEventHandler(
Script.goBeginningOfLine,
# Translators: this command will move the user to the
# beginning of the line in an HTML document.
#
_("Goes to the beginning of the line."))
self.inputEventHandlers["goEndOfLineHandler"] = \
input_event.InputEventHandler(
Script.goEndOfLine,
# Translators: this command will move the user to the
# end of the line in an HTML document.
#
_("Goes to the end of the line."))
self.inputEventHandlers["expandComboBoxHandler"] = \
input_event.InputEventHandler(
Script.expandComboBox,
# Translators: this is for causing a collapsed combo box
# which was reached by Orca's caret navigation to be expanded.
#
_("Causes the current combo box to be expanded."))
self.inputEventHandlers["advanceLivePoliteness"] = \
input_event.InputEventHandler(
Script.advanceLivePoliteness,
# Translators: this is for advancing the live regions
# politeness setting
#
_("Advance live region politeness setting."))
self.inputEventHandlers["setLivePolitenessOff"] = \
input_event.InputEventHandler(
Script.setLivePolitenessOff,
# Translators: this is for setting all live regions
# to 'off' politeness.
#
_("Set default live region politeness level to off."))
self.inputEventHandlers["monitorLiveRegions"] = \
input_event.InputEventHandler(
Script.monitorLiveRegions,
# Translators: this is a toggle to monitor live regions
# or not.
#
_("Monitor live regions."))
self.inputEventHandlers["reviewLiveAnnouncement"] = \
input_event.InputEventHandler(
Script.reviewLiveAnnouncement,
# Translators: this is for reviewing up to nine stored
# previous live messages.
#
_("Review live region announcement."))
self.inputEventHandlers["goPreviousObjectInOrderHandler"] = \
input_event.InputEventHandler(
Script.goPreviousObjectInOrder,
# Translators: this is for navigating between objects
# (regardless of type) in HTML
#
_("Goes to the previous object."))
self.inputEventHandlers["goNextObjectInOrderHandler"] = \
input_event.InputEventHandler(
Script.goNextObjectInOrder,
# Translators: this is for navigating between objects
# (regardless of type) in HTML
#
_("Goes to the next object."))
self.inputEventHandlers["toggleCaretNavigationHandler"] = \
input_event.InputEventHandler(
Script.toggleCaretNavigation,
# Translators: Gecko native caret navigation is where
# Firefox itself controls how the arrow keys move the caret
# around HTML content. It's often broken, so Orca needs
# to provide its own support. As such, Orca offers the user
# the ability to switch between the Firefox mode and the
# Orca mode.
#
_("Switches between Gecko native and Orca caret navigation."))
self.inputEventHandlers["sayAllHandler"] = \
input_event.InputEventHandler(
Script.sayAll,
# Translators: the Orca "SayAll" command allows the
# user to press a key and have the entire document in
# a window be automatically spoken to the user. If
# the user presses any key during a SayAll operation,
# the speech will be interrupted and the cursor will
# be positioned at the point where the speech was
# interrupted.
#
_("Speaks entire document."))
self.inputEventHandlers["panBrailleLeftHandler"] = \
input_event.InputEventHandler(
Script.panBrailleLeft,
# Translators: a refreshable braille display is an
# external hardware device that presents braille
# character to the user. There are a limited number
# of cells on the display (typically 40 cells). Orca
# provides the feature to build up a longer logical
# line and allow the user to press buttons on the
# braille display so they can pan left and right over
# this line.
#
_("Pans the braille display to the left."),
False) # Do not enable learn mode for this action
self.inputEventHandlers["panBrailleRightHandler"] = \
input_event.InputEventHandler(
Script.panBrailleRight,
# Translators: a refreshable braille display is an
# external hardware device that presents braille
# character to the user. There are a limited number
# of cells on the display (typically 40 cells). Orca
# provides the feature to build up a longer logical
# line and allow the user to press buttons on the
# braille display so they can pan left and right over
# this line.
#
_("Pans the braille display to the right."),
False) # Do not enable learn mode for this action
self.inputEventHandlers["moveToMouseOverHandler"] = \
input_event.InputEventHandler(
Script.moveToMouseOver,
# Translators: hovering the mouse over certain objects
# on a web page causes a new object to appear such as
# a pop-up menu. This command will move the user to the
# object which just appeared as a result of the user
# hovering the mouse. If the user is already in the
# mouse over object, this command will hide the mouse
# over and return the user to the object he/she was in.
#
_("Moves focus into and away from the current mouse over."))
def getListeners(self):
"""Sets up the AT-SPI event listeners for this script.
"""
listeners = default.Script.getListeners(self)
listeners["document:reload"] = \
self.onDocumentReload
listeners["document:load-complete"] = \
self.onDocumentLoadComplete
listeners["document:load-stopped"] = \
self.onDocumentLoadStopped
listeners["object:state-changed:showing"] = \
self.onStateChanged
listeners["object:state-changed:checked"] = \
self.onStateChanged
listeners["object:state-changed:indeterminate"] = \
self.onStateChanged
listeners["object:state-changed:busy"] = \
self.onStateChanged
listeners["object:children-changed"] = \
self.onChildrenChanged
listeners["object:text-changed:insert"] = \
self.onTextInserted
listeners["object:state-changed:focused"] = \
self.onStateFocused
return listeners
def __getArrowBindings(self):
"""Returns an instance of keybindings.KeyBindings that use the
arrow keys for navigating HTML content.
"""
keyBindings = keybindings.KeyBindings()
keyBindings.load(keymaps.arrowKeymap, self.inputEventHandlers)
return keyBindings
def getKeyBindings(self):
"""Defines the key bindings for this script.
Returns an instance of keybindings.KeyBindings.
"""
keyBindings = default.Script.getKeyBindings(self)
# load common keymap
keyBindings.load(keymaps.commonKeymap, self.inputEventHandlers)
if _settingsManager.getSetting('keyboardLayout') == \
orca.settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP:
keyBindings.load(keymaps.desktopKeymap, self.inputEventHandlers)
else:
keyBindings.load(keymaps.laptopKeymap, self.inputEventHandlers)
if script_settings.controlCaretNavigation:
for keyBinding in self.__getArrowBindings().keyBindings:
keyBindings.add(keyBinding)
bindings = self.structuralNavigation.keyBindings
for keyBinding in bindings.keyBindings:
keyBindings.add(keyBinding)
return keyBindings
def getAppPreferencesGUI(self):
"""Return a GtkVBox contain the application unique configuration
GUI items for the current application.
"""
vbox = gtk.VBox(False, 0)
vbox.set_border_width(12)
gtk.Widget.show(vbox)
# General ("Page") Navigation frame.
#
generalFrame = gtk.Frame()
gtk.Widget.show(generalFrame)
gtk.Box.pack_start(vbox, generalFrame, False, False, 5)
generalAlignment = gtk.Alignment(0.5, 0.5, 1, 1)
gtk.Widget.show(generalAlignment)
gtk.Container.add(generalFrame, generalAlignment)
gtk.Alignment.set_padding(generalAlignment, 0, 0, 12, 0)
generalVBox = gtk.VBox(False, 0)
gtk.Widget.show(generalVBox)
gtk.Container.add(generalAlignment, generalVBox)
# Translators: Gecko native caret navigation is where
# Firefox itself controls how the arrow keys move the caret
# around HTML content. It's often broken, so Orca needs
# to provide its own support. As such, Orca offers the user
# the ability to switch between the Firefox mode and the
# Orca mode.
#
label = _("Use _Orca Caret Navigation")
self.controlCaretNavigationCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.controlCaretNavigationCheckButton)
gtk.Box.pack_start(generalVBox,
self.controlCaretNavigationCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(self.controlCaretNavigationCheckButton,
script_settings.controlCaretNavigation)
# Translators: Orca provides keystrokes to navigate HTML content
# in a structural manner: go to previous/next header, list item,
# table, etc.
#
label = _("Use Orca _Structural Navigation")
self.structuralNavigationCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.structuralNavigationCheckButton)
gtk.Box.pack_start(generalVBox, self.structuralNavigationCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(self.structuralNavigationCheckButton,
self.structuralNavigation.enabled)
# Translators: Orca has had to implement its own caret navigation
# model to work around issues in Gecko/Firefox. In certain versions
# of Firefox, we must perform a focus grab on each object being
# navigated in order for things to work as expected; in other
# versions of Firefox, we must avoid doing so in order for things
# to work as expected. We cannot identify with certainty which
# situation the user is in, so we must provide this as an option
# within Orca.
#
label = _("_Grab focus on objects when navigating")
self.grabFocusOnAncestorCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.grabFocusOnAncestorCheckButton)
gtk.Box.pack_start(generalVBox, self.grabFocusOnAncestorCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(self.grabFocusOnAncestorCheckButton,
script_settings.grabFocusOnAncestor)
# Translators: when the user arrows up and down in HTML content,
# it is some times beneficial to always position the cursor at the
# beginning of the line rather than guessing the position directly
# above the current cursor position. This option allows the user
# to decide the behavior they want.
#
label = \
_("_Position cursor at start of line when navigating vertically")
self.arrowToLineBeginningCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.arrowToLineBeginningCheckButton)
gtk.Box.pack_start(generalVBox, self.arrowToLineBeginningCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(self.arrowToLineBeginningCheckButton,
script_settings.arrowToLineBeginning)
# Translators: when the user loads a new page in Firefox, they
# can optionally tell Orca to automatically start reading a
# page from beginning to end.
#
label = \
_("Automatically start speaking a page when it is first _loaded")
self.sayAllOnLoadCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.sayAllOnLoadCheckButton)
gtk.Box.pack_start(generalVBox, self.sayAllOnLoadCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(self.sayAllOnLoadCheckButton,
script_settings.sayAllOnLoad)
# Translators: this is the title of a panel holding options for
# how to navigate HTML content (e.g., Orca caret navigation,
# positioning of caret, etc.).
#
generalLabel = gtk.Label("%s" % _("Page Navigation"))
gtk.Widget.show(generalLabel)
gtk.Frame.set_label_widget(generalFrame, generalLabel)
gtk.Label.set_use_markup(generalLabel, True)
# Table Navigation frame.
#
tableFrame = gtk.Frame()
gtk.Widget.show(tableFrame)
gtk.Box.pack_start(vbox, tableFrame, False, False, 5)
tableAlignment = gtk.Alignment(0.5, 0.5, 1, 1)
gtk.Widget.show(tableAlignment)
gtk.Container.add(tableFrame, tableAlignment)
gtk.Alignment.set_padding(tableAlignment, 0, 0, 12, 0)
tableVBox = gtk.VBox(False, 0)
gtk.Widget.show(tableVBox)
gtk.Container.add(tableAlignment, tableVBox)
# Translators: this is an option to tell Orca whether or not it
# should speak table cell coordinates in document content.
#
label = _("Speak _cell coordinates")
self.speakCellCoordinatesCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.speakCellCoordinatesCheckButton)
gtk.Box.pack_start(tableVBox, self.speakCellCoordinatesCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(
self.speakCellCoordinatesCheckButton,
_settingsManager.getSetting('speakCellCoordinates'))
# Translators: this is an option to tell Orca whether or not it
# should speak the span size of a table cell (e.g., how many
# rows and columns a particular table cell spans in a table).
#
label = _("Speak _multiple cell spans")
self.speakCellSpanCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.speakCellSpanCheckButton)
gtk.Box.pack_start(tableVBox, self.speakCellSpanCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(
self.speakCellSpanCheckButton,
_settingsManager.getSetting('speakCellSpan'))
# Translators: this is an option for whether or not to speak
# the header of a table cell in document content.
#
label = _("Announce cell _header")
self.speakCellHeadersCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.speakCellHeadersCheckButton)
gtk.Box.pack_start(tableVBox, self.speakCellHeadersCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(
self.speakCellHeadersCheckButton,
_settingsManager.getSetting('speakCellHeaders'))
# Translators: this is an option to allow users to skip over
# empty/blank cells when navigating tables in document content.
#
label = _("Skip _blank cells")
self.skipBlankCellsCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.skipBlankCellsCheckButton)
gtk.Box.pack_start(tableVBox, self.skipBlankCellsCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(
self.skipBlankCellsCheckButton,
_settingsManager.getSetting('skipBlankCells'))
# Translators: this is the title of a panel containing options
# for specifying how to navigate tables in document content.
#
tableLabel = gtk.Label("%s" % _("Table Navigation"))
gtk.Widget.show(tableLabel)
gtk.Frame.set_label_widget(tableFrame, tableLabel)
gtk.Label.set_use_markup(tableLabel, True)
# Find Options frame.
#
findFrame = gtk.Frame()
gtk.Widget.show(findFrame)
gtk.Box.pack_start(vbox, findFrame, False, False, 5)
findAlignment = gtk.Alignment(0.5, 0.5, 1, 1)
gtk.Widget.show(findAlignment)
gtk.Container.add(findFrame, findAlignment)
gtk.Alignment.set_padding(findAlignment, 0, 0, 12, 0)
findVBox = gtk.VBox(False, 0)
gtk.Widget.show(findVBox)
gtk.Container.add(findAlignment, findVBox)
# Translators: this is an option to allow users to have Orca
# automatically speak the line that contains the match while
# the user is still in Firefox's Find toolbar.
#
label = _("Speak results during _find")
self.speakResultsDuringFindCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.speakResultsDuringFindCheckButton)
gtk.Box.pack_start(findVBox, self.speakResultsDuringFindCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(self.speakResultsDuringFindCheckButton,
script_settings.speakResultsDuringFind)
# Translators: this is an option which dictates whether the line
# that contains the match from the Find toolbar should always
# be spoken, or only spoken if it is a different line than the
# line which contained the last match.
#
label = _("Onl_y speak changed lines during find")
self.changedLinesOnlyCheckButton = gtk.CheckButton(label)
gtk.Widget.show(self.changedLinesOnlyCheckButton)
gtk.Box.pack_start(findVBox, self.changedLinesOnlyCheckButton,
False, False, 0)
gtk.ToggleButton.set_active(self.changedLinesOnlyCheckButton,
script_settings.onlySpeakChangedLinesDuringFind)
hbox = gtk.HBox(False, 0)
gtk.Widget.show(hbox)
gtk.Box.pack_start(findVBox, hbox, False, False, 0)
# Translators: this option allows the user to specify the number
# of matched characters that must be present before Orca speaks
# the line that contains the results from the Find toolbar.
#
self.minimumFindLengthLabel = \
gtk.Label(_("Minimum length of matched text:"))
self.minimumFindLengthLabel.set_alignment(0, 0.5)
gtk.Widget.show(self.minimumFindLengthLabel)
gtk.Box.pack_start(hbox, self.minimumFindLengthLabel, False, False, 5)
self.minimumFindLengthAdjustment = \
gtk.Adjustment(script_settings.minimumFindLength, 0, 20, 1)
self.minimumFindLengthSpinButton = \
gtk.SpinButton(self.minimumFindLengthAdjustment, 0.0, 0)
gtk.Widget.show(self.minimumFindLengthSpinButton)
gtk.Box.pack_start(hbox, self.minimumFindLengthSpinButton,
False, False, 5)
acc_targets = []
acc_src = self.minimumFindLengthLabel.get_accessible()
relation_set = acc_src.ref_relation_set()
acc_targ = self.minimumFindLengthSpinButton.get_accessible()
acc_targets.append(acc_targ)
relation = atk.Relation(acc_targets, 1)
relation.set_property('relation-type', atk.RELATION_LABEL_FOR)
relation_set.add(relation)
# Translators: this is the title of a panel containing options
# for using Firefox's Find toolbar.
#
findLabel = gtk.Label("%s" % _("Find Options"))
gtk.Widget.show(findLabel)
gtk.Frame.set_label_widget(findFrame, findLabel)
gtk.Label.set_use_markup(findLabel, True)
return vbox
def setAppPreferences(self, prefs):
"""Write out the application specific preferences lines and set the
new values.
Arguments:
- prefs: file handle for application preferences.
"""
prefs.writelines("\n")
prefix = "orca.scripts.toolkits.Gecko.script_settings"
prefs.writelines("import %s\n\n" % prefix)
value = self.controlCaretNavigationCheckButton.get_active()
prefs.writelines("%s.controlCaretNavigation = %s\n" % (prefix, value))
script_settings.controlCaretNavigation = value
value = self.structuralNavigationCheckButton.get_active()
prefs.writelines("%s.structuralNavigationEnabled = %s\n" \
% (prefix, value))
script_settings.structuralNavigationEnabled = value
value = self.grabFocusOnAncestorCheckButton.get_active()
prefs.writelines("%s.grabFocusOnAncestor = %s\n" % (prefix, value))
script_settings.grabFocusOnAncestor = value
value = self.arrowToLineBeginningCheckButton.get_active()
prefs.writelines("%s.arrowToLineBeginning = %s\n" % (prefix, value))
script_settings.arrowToLineBeginning = value
value = self.sayAllOnLoadCheckButton.get_active()
prefs.writelines("%s.sayAllOnLoad = %s\n" % (prefix, value))
script_settings.sayAllOnLoad = value
value = self.speakResultsDuringFindCheckButton.get_active()
prefs.writelines("%s.speakResultsDuringFind = %s\n" % (prefix, value))
script_settings.speakResultsDuringFind = value
value = self.changedLinesOnlyCheckButton.get_active()
prefs.writelines("%s.onlySpeakChangedLinesDuringFind = %s\n"\
% (prefix, value))
script_settings.onlySpeakChangedLinesDuringFind = value
value = self.minimumFindLengthSpinButton.get_value()
prefs.writelines("%s.minimumFindLength = %s\n" % (prefix, value))
script_settings.minimumFindLength = value
# These structural navigation settings used to be application-
# specific preferences because at the time structural navigation
# was implemented it was part of the Gecko script. These settings
# are now part of settings.py so that other scripts can implement
# structural navigation. But until that happens, there's no need
# to move these controls/change the preferences dialog.
#
value = self.speakCellCoordinatesCheckButton.get_active()
prefs.writelines("orca.settings.speakCellCoordinates = %s\n" % value)
_settingsManager.setSetting('speakCellCoordinates', value)
value = self.speakCellSpanCheckButton.get_active()
prefs.writelines("orca.settings.speakCellSpan = %s\n" % value)
_settingsManager.setSetting('speakCellSpan', value)
value = self.speakCellHeadersCheckButton.get_active()
prefs.writelines("orca.settings.speakCellHeaders = %s\n" % value)
_settingsManager.setSetting('speakCellHeaders', value)
value = self.skipBlankCellsCheckButton.get_active()
prefs.writelines("orca.settings.skipBlankCells = %s\n" % value)
_settingsManager.setSetting('skipBlankCells', value)
def getAppState(self):
"""Returns an object that can be passed to setAppState. This
object will be use by setAppState to restore any state information
that was being maintained by the script."""
return [default.Script.getAppState(self),
self._documentFrameCaretContext]
def setAppState(self, appState):
"""Sets the application state using the given appState object.
Arguments:
- appState: an object obtained from getAppState
"""
try:
[defaultAppState,
self._documentFrameCaretContext] = appState
default.Script.setAppState(self, defaultAppState)
except:
debug.printException(debug.LEVEL_WARNING)
def consumesKeyboardEvent(self, keyboardEvent):
"""Called when a key is pressed on the keyboard.
Arguments:
- keyboardEvent: an instance of input_event.KeyboardEvent
Returns True if the event is of interest.
"""
# 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 = {}
# The reason we override this method is that we only want
# to consume keystrokes under certain conditions. For
# example, we only control the arrow keys when we're
# managing caret navigation and we're inside document content.
#
# [[[TODO: WDW - this might be broken when we're inside a
# text area that's inside document (or anything else that
# we want to allow to control its own destiny).]]]
user_bindings = None
user_bindings_map = _settingsManager.getSetting('keyBindingsMap')
if self.__module__ in user_bindings_map:
user_bindings = user_bindings_map[self.__module__]
elif "default" in user_bindings_map:
user_bindings = user_bindings_map["default"]
consumes = False
if user_bindings:
handler = user_bindings.getInputHandler(keyboardEvent)
if handler and handler.function in self._caretNavigationFunctions:
return self.useCaretNavigationModel(keyboardEvent)
elif handler \
and (handler.function in self.structuralNavigation.functions \
or handler.function in self._liveRegionFunctions):
return self.useStructuralNavigationModel()
else:
consumes = handler != None
if not consumes:
handler = self.keyBindings.getInputHandler(keyboardEvent)
if handler and handler.function in self._caretNavigationFunctions:
return self.useCaretNavigationModel(keyboardEvent)
elif handler \
and (handler.function in self.structuralNavigation.functions \
or handler.function in self._liveRegionFunctions):
return self.useStructuralNavigationModel()
else:
consumes = handler != None
return consumes
def textLines(self, obj):
"""Creates a generator that can be used to iterate over each line
of a text object, starting at the caret offset.
Arguments:
- obj: an Accessible that has a text specialization
Returns an iterator that produces elements of the form:
[SayAllContext, acss], where SayAllContext has the text to be
spoken and acss is an ACSS instance for speaking the text.
"""
# Determine the correct "say all by" mode to use.
#
sayAllStyle = _settingsManager.getSetting('sayAllStyle')
sayAllBySentence = sayAllStyle == settings.SAYALL_STYLE_SENTENCE
[obj, characterOffset] = self.getCaretContext()
if sayAllBySentence:
# Attempt to locate the start of the current sentence by
# searching to the left for a sentence terminator. If we don't
# find one, or if the "say all by" mode is not sentence, we'll
# just start the sayAll from at the beginning of this line/object.
#
text = self.utilities.queryNonEmptyText(obj)
if text:
[line, startOffset, endOffset] = \
text.getTextAtOffset(characterOffset,
pyatspi.TEXT_BOUNDARY_LINE_START)
beginAt = 0
if line.strip():
terminators = ['. ', '? ', '! ']
for terminator in terminators:
try:
index = line.rindex(terminator,
0,
characterOffset - startOffset)
if index > beginAt:
beginAt = index
except:
pass
characterOffset = startOffset + beginAt
else:
[obj, characterOffset] = \
self.findNextCaretInOrder(obj, characterOffset)
done = False
while not done:
if sayAllBySentence:
contents = self.getObjectContentsAtOffset(obj, characterOffset)
else:
contents = self.getLineContentsAtOffset(obj, characterOffset)
utterances = self.getUtterancesFromContents(contents)
clumped = self.clumpUtterances(utterances)
for i in xrange(len(clumped)):
[obj, startOffset, endOffset, text] = \
contents[min(i, len(contents)-1)]
[element, voice] = clumped[i]
if isinstance(element, basestring):
element = self.utilities.adjustForRepeats(element)
if isinstance(element, (Pause, ACSS)):
# At the moment, SayAllContext is expecting a string; not
# a Pause. For now, being conservative and catching that
# here. See bug #591351.
#
continue
yield [speechserver.SayAllContext(obj, element,
startOffset, endOffset),
voice]
obj = contents[-1][0]
characterOffset = max(0, contents[-1][2] - 1)
if sayAllBySentence:
[obj, characterOffset] = \
self.findNextCaretInOrder(obj, characterOffset)
else:
[obj, characterOffset] = \
self.findNextLine(obj, characterOffset)
done = (obj == None)
def __sayAllProgressCallback(self, context, callbackType):
if callbackType == speechserver.SayAllContext.PROGRESS:
#print "PROGRESS", context.utterance, context.currentOffset
#
# Attempt to keep the content visible on the screen as
# it is being read, but avoid links as grabFocus sometimes
# makes them disappear and sayAll to subsequently stop.
#
if context.currentOffset == 0 and \
context.obj.getRole() in [pyatspi.ROLE_HEADING,
pyatspi.ROLE_SECTION,
pyatspi.ROLE_PARAGRAPH] \
and context.obj.parent.getRole() != pyatspi.ROLE_LINK:
characterCount = context.obj.queryText().characterCount
self.setCaretPosition(context.obj, characterCount-1)
elif callbackType == speechserver.SayAllContext.INTERRUPTED:
#print "INTERRUPTED", context.utterance, context.currentOffset
try:
self.setCaretPosition(context.obj, context.currentOffset)
except:
characterCount = context.obj.queryText().characterCount
self.setCaretPosition(context.obj, characterCount-1)
self.updateBraille(context.obj)
elif callbackType == speechserver.SayAllContext.COMPLETED:
#print "COMPLETED", context.utterance, context.currentOffset
try:
self.setCaretPosition(context.obj, context.currentOffset)
except:
characterCount = context.obj.queryText().characterCount
self.setCaretPosition(context.obj, characterCount-1)
self.updateBraille(context.obj)
def presentFindResults(self, obj, offset):
"""Updates the caret context to the match indicated by obj and
offset. Then presents the results according to the user's
preferences.
Arguments:
-obj: The accessible object within the document
-offset: The offset with obj where the caret should be positioned
"""
# At some point in Firefox 3.2 we started getting detail1 values of
# -1 for the caret-moved events for unfocused content during a find.
# We don't want to base the new caret offset -- or the current line
# on this value. We should be able to count on the selection range
# instead -- across FF 3.0, 3.1, and 3.2.
#
enoughSelected = False
text = self.utilities.queryNonEmptyText(obj)
if text and text.getNSelections():
[start, end] = text.getSelection(0)
offset = max(offset, start)
if end - start >= script_settings.minimumFindLength:
enoughSelected = True
# Haing done that, update the caretContext. If the user wants
# matches spoken, we also need to if we are on the same line
# as before.
#
origObj, origOffset = self.getCaretContext()
self.setCaretContext(obj, offset)
if enoughSelected and script_settings.speakResultsDuringFind:
origExtents = self.getExtents(origObj, origOffset - 1, origOffset)
newExtents = self.getExtents(obj, offset - 1, offset)
lineChanged = not self.onSameLine(origExtents, newExtents)
# If the user starts backspacing over the text in the
# toolbar entry, he/she is indicating they want to perform
# a different search. Because madeFindAnnounement may
# be set to True, we should reset it -- but only if we
# detect the line has also changed. We're not getting
# events from the Find entry, so we have to compare
# offsets.
#
if self.utilities.isSameObject(origObj, obj) \
and (origOffset > offset) and lineChanged:
self.madeFindAnnouncement = False
if lineChanged or not self.madeFindAnnouncement or \
not script_settings.onlySpeakChangedLinesDuringFind:
line = self.getLineContentsAtOffset(obj, offset)
self.speakContents(line)
self.madeFindAnnouncement = True
def sayAll(self, inputEvent):
"""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.inDocumentContent():
return default.Script.sayAll(self, inputEvent)
else:
speech.sayAll(self.textLines(orca_state.locusOfFocus),
self.__sayAllProgressCallback)
return True
def onCaretMoved(self, event):
"""Caret movement in Gecko is somewhat unreliable and
unpredictable, but we need to handle it. When we detect caret
movement, we make sure we update our own notion of the caret
position: our caretContext is an [obj, characterOffset] that
points to our current item and character (if applicable) of
interest. If our current item doesn't implement the
accessible text specialization, the characterOffset value
is meaningless (and typically -1)."""
eventSourceRole = event.source.getRole()
eventSourceState = event.source.getState()
eventSourceInDocument = self.inDocumentContent(event.source)
try:
locusOfFocusRole = orca_state.locusOfFocus.getRole()
locusOfFocusState = orca_state.locusOfFocus.getState()
except:
locusOfFocusRole = None
locusOfFocusState = pyatspi.StateSet()
locusOfFocusState = locusOfFocusState.raw()
notify = False
# Find out if the caret really moved. Firefox 3.1 gives us caret-moved
# events when certain focusable objects first get focus. If we haven't
# really moved, there's no point in updating braille again -- which is
# what we'll wind up doing if this event reaches the default script.
#
[obj, characterOffset] = self.getCaretContext()
if max(0, characterOffset) == event.detail1 \
and self.utilities.isSameObject(obj, event.source):
return
if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
string, mods = self.utilities.lastKeyAndModifiers()
if self.useCaretNavigationModel(orca_state.lastInputEvent):
# Orca is set to control the caret and is in a place where
# doing so is appropriate. Therefore, this event is likely
# extraneous and can be ignored. Exceptions:
#
# 1. If the object is an entry and useCaretNavigationModel is
# true, then we must be at the edge of the entry and about
# to exit it (returning to the document).
# 2. If the locusOfFocus was a same-page link, we will get a
# caret moved event for some object within the document
# frame.
#
if not self.utilities.isEntry(event.source) \
and self.utilities.isSameObject(
event.source, orca_state.locusOfFocus):
return
# We are getting extraneous events that are not being caught
# by the above, and which are causing us to loop. See bug
# #552887. This is admittedly a rather broad check. However,
# if we're here it's because we're controlling the caret in
# which case we don't expect to get caret moved events of
# interest other than those mentioned above.
#
if locusOfFocusRole == pyatspi.ROLE_IMAGE:
return
elif locusOfFocusRole == pyatspi.ROLE_LINK:
# Be sure it's not a same-page link. While such beasts
# typically point to anchors, they can point to other
# objects referencing them by name or ID. Therefore,
# get the URI for the link of interest and parse it.
# parsed URI is returned as a tuple containing six
# components:
# scheme://netloc/path;parameters?query#fragment.
try:
uri = self.utilities.uri(orca_state.locusOfFocus)
uriInfo = urlparse.urlparse(uri)
except:
pass
else:
if uriInfo and not uriInfo[5]:
return
else:
notify = True
elif eventSourceRole == pyatspi.ROLE_SECTION:
# Google Calendar's Day grid seems to issue these quite
# a bit. If we don't ignore them, we'll loop.
#
return
elif not self.isNavigableAria(event.source):
if script_settings.controlCaretNavigation:
return
elif self.isAriaWidget(orca_state.locusOfFocus) \
and self.utilities.isSameObject(event.source,
orca_state.locusOfFocus.parent):
return
elif eventSourceInDocument and not self.inDocumentContent() \
and orca_state.locusOfFocus:
# This is an indication that soemthing else is moving
# the caret on our behalf, such as a help window, the
# Find toolbar, the UIUC accessiblity extension, etc.
# If that's the case, we want to update our position.
# If we're in the Find toolbar, we also want to present
# the results.
#
if self.utilities.inFindToolbar():
self.presentFindResults(event.source, event.detail1)
else:
self.setCaretContext(event.source, event.detail1)
return
# If we're still here, and in document content, update the caret
# context and set the locusOfFocus so that the default script's
# onCaretMoved will handle.
#
if eventSourceInDocument and not self.isAriaWidget(event.source):
if not self.utilities.isEntry(event.source):
[obj, characterOffset] = \
self.findFirstCaretContext(event.source, event.detail1)
else:
[obj, characterOffset] = [event.source, event.detail1]
self.setCaretContext(obj, characterOffset)
orca.setLocusOfFocus(event, obj, notifyScript=notify)
if notify:
# No point in double-brailling the locusOfFocus.
#
return
# Pass the event along to the default script for processing.
#
default.Script.onCaretMoved(self, event)
def onTextDeleted(self, event):
"""Called whenever text is from an an object.
Arguments:
- event: the Event
"""
self._destroyLineCache()
# If text is removed from something which is not editable, trash our
# saved guessed labels to be on the safe side.
#
if not event.source.getState().contains(pyatspi.STATE_EDITABLE):
self._guessedLabels = {}
if self.inMouseOverObject:
obj = self.lastMouseOverObject
while obj and (obj != obj.parent):
if self.utilities.isSameObject(event.source, obj):
self.restorePreMouseOverContext()
break
obj = obj.parent
default.Script.onTextDeleted(self, event)
def onTextInserted(self, event):
"""Called whenever text is inserted into an object.
Arguments:
- event: the Event
"""
self._destroyLineCache()
# If text is inserted into something which is not editable, trash our
# saved guessed labels to be on the safe side.
#
if not event.source.getState().contains(pyatspi.STATE_EDITABLE):
self._guessedLabels = {}
# handle live region events
if self.handleAsLiveRegion(event):
self.liveMngr.handleEvent(event)
return
default.Script.onTextInserted(self, event)
def _getCtrlShiftSelectionsStrings(self):
return [
# Translators: when the user selects (highlights) text in
# a document, Orca will speak information about what they
# have selected.
#
_("line selected down from cursor position"),
_("line unselected down from cursor position"),
_("line selected up from cursor position"),
_("line unselected up from cursor position"),
]
def onTextSelectionChanged(self, event):
"""Called when an object's text selection changes.
Arguments:
- event: the Event
"""
if not self.inDocumentContent(orca_state.locusOfFocus) \
and self.inDocumentContent(event.source):
return
default.Script.onTextSelectionChanged(self, event)
def onChildrenChanged(self, event):
"""Called when a child node has changed. In particular, we are looking
for addition events often associated with Javascipt insertion. One such
such example would be the programmatic insertion of a tooltip or alert
dialog."""
# If children are being added or removed, trash our saved guessed
# labels to be on the safe side.
#
self._guessedLabels = {}
# no need moving forward if we don't have our target.
if event.any_data is None:
return
# If we just routed the mouse pointer to our current location,
# we should say something about what resulted.
#
if self.lastMouseRoutingTime \
and 0 < time.time() - self.lastMouseRoutingTime < 1 \
and event.type.startswith("object:children-changed:add"):
utterances = []
# Translators: Orca has a command that moves the mouse
# pointer to the current location on a web page. If
# moving the mouse pointer caused an item to appear
# such as a pop-up menu, we want to present that fact.
#
utterances.append(_("New item has been added"))
utterances.extend(
self.speechGenerator.generateSpeech(event.any_data,
force = True))
speech.speak(utterances)
self.lastMouseOverObject = event.any_data
self.preMouseOverContext = self.getCaretContext()
return
# handle live region events
if self.handleAsLiveRegion(event):
self.liveMngr.handleEvent(event)
return
if event.type.startswith("object:children-changed:add") \
and event.any_data.getRole() == pyatspi.ROLE_ALERT \
and event.source.getRole() in [pyatspi.ROLE_SCROLL_PANE,
pyatspi.ROLE_FRAME]:
utterances = []
utterances.append(rolenames.getSpeechForRoleName(event.any_data))
verbosity = _settingsManager.getSetting('speechVerbosityLevel')
if verbosity == settings.VERBOSITY_LEVEL_VERBOSE:
utterances.extend(
self.speechGenerator.generateSpeech(event.any_data))
speech.speak(utterances)
def onDocumentReload(self, event):
"""Called when the reload button is hit for a web page."""
# We care about the main document and we'll ignore document
# events from HTML iframes.
#
if event.source.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
self._loadingDocumentContent = True
def onDocumentLoadComplete(self, event):
"""Called when a web page load is completed."""
# We care about the main document and we'll ignore document
# events from HTML iframes.
#
if event.source.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
# Reset the live region manager.
self.liveMngr.reset()
self._loadingDocumentContent = False
self._loadingDocumentTime = time.time()
def onDocumentLoadStopped(self, event):
"""Called when a web page load is interrupted."""
# We care about the main document and we'll ignore document
# events from HTML iframes.
#
if event.source.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
self._loadingDocumentContent = False
self._loadingDocumentTime = time.time()
def onNameChanged(self, event):
"""Called whenever a property on an object changes.
Arguments:
- event: the Event
"""
if event.source.getRole() == pyatspi.ROLE_FRAME:
self.liveMngr.flushMessages()
def onFocus(self, event):
"""Called whenever an object gets focus.
Arguments:
- event: the Event
"""
try:
eventSourceRole = event.source.getRole()
except:
return
# Ignore events on the frame as they are often intermingled
# with menu activity, wreaking havoc on the context. We will
# ignore autocompletes because we get focus events for the
# entry, which is the thing that really has focus anyway.
#
if eventSourceRole in [pyatspi.ROLE_FRAME,
pyatspi.ROLE_AUTOCOMPLETE]:
return
# If this event is the result of our calling grabFocus() on
# this object in setCaretPosition(), we want to ignore it
# unless it happens to be the same object as our current
# caret context.
#
if self.utilities.isSameObject(event.source, self._objectForFocusGrab):
[obj, characterOffset] = self.getCaretContext()
if not self.utilities.isSameObject(event.source, obj):
return
self._objectForFocusGrab = None
# We also ignore focus events on the panel that holds the document
# frame. We end up getting these typically because we've called
# grabFocus on this panel when we're doing caret navigation. In
# those cases, we want the locus of focus to be the subcomponent
# that really holds the caret.
#
if eventSourceRole == pyatspi.ROLE_PANEL:
documentFrame = self.utilities.documentFrame()
if documentFrame and (documentFrame.parent == event.source):
return
else:
# Web pages can contain their own panels. If the locus
# of focus is within that panel, we probably moved off
# of a focusable item (like a link within the panel)
# to something non-focusable (like text within the panel).
# If we don't ignore this event, we'll loop to the top
# of the panel.
#
containingPanel = self.utilities.ancestorWithRole(
orca_state.locusOfFocus,
[pyatspi.ROLE_PANEL],
[pyatspi.ROLE_DOCUMENT_FRAME])
if self.utilities.isSameObject(containingPanel, event.source):
return
# When we get a focus event on the document frame, it's usually
# because we did a grabFocus on its parent in setCaretPosition.
# We try to handle this here by seeing if there is already a
# caret context for the document frame. If we succeed, then
# we set the focus on the object that's holding the caret.
#
if eventSourceRole == pyatspi.ROLE_DOCUMENT_FRAME \
and not event.source.getState().contains(pyatspi.STATE_EDITABLE):
try:
[obj, characterOffset] = self.getCaretContext()
state = obj.getState()
if not state.contains(pyatspi.STATE_FOCUSED):
if not state.contains(pyatspi.STATE_FOCUSABLE) \
or not self.inDocumentContent():
orca.setLocusOfFocus(event, obj)
return
except:
pass
elif eventSourceRole != pyatspi.ROLE_LINK \
and self.inDocumentContent(event.source) \
and not self.isAriaWidget(event.source):
[obj, characterOffset] = \
self.findFirstCaretContext(event.source, 0)
self.setCaretContext(obj, characterOffset)
if not self.utilities.isSameObject(event.source, obj):
if not self.utilities.isSameObject(
obj, orca_state.locusOfFocus):
orca.setLocusOfFocus(event, obj, notifyScript=False)
# If an alert got focus, let's do the best we can to
# try to automatically speak its contents while also
# making sure the locus of focus and caret context
# are in the right spot for braille and caret navigation.
# http://bugzilla.gnome.org/show_bug.cgi?id=570551
#
if eventSourceRole == pyatspi.ROLE_ALERT:
speech.speak(self.speechGenerator.generateSpeech(
event.source))
self.updateBraille(obj)
else:
self.presentLine(obj, characterOffset)
return
default.Script.onFocus(self, event)
def onLinkSelected(self, event):
"""Called when a link gets selected. Note that in Firefox 3,
link selected events are not issued when a link is selected.
Instead, a focus: event is issued. This is 'old' code left
over from Yelp and Firefox 2.
Arguments:
- event: the Event
"""
text = self.utilities.queryNonEmptyText(event.source)
hypertext = event.source.queryHypertext()
linkIndex = self.utilities.linkIndex(event.source, text.caretOffset)
if linkIndex >= 0:
link = hypertext.getLink(linkIndex)
linkText = text.getText(link.startIndex, link.endIndex)
#[string, startOffset, endOffset] = text.getTextAtOffset(
# text.caretOffset,
# pyatspi.TEXT_BOUNDARY_LINE_START)
#print "onLinkSelected", event.source.getRole() , string,
#print " caretOffset: ", text.caretOffset
#print " line startOffset:", startOffset
#print " line endOffset: ", startOffset
#print " caret in line: ", text.caretOffset - startOffset
speech.speak(linkText, self.voices[settings.HYPERLINK_VOICE])
elif text:
# We'll just assume the whole thing is a link. This happens
# in yelp when we navigate the table of contents of something
# like the Desktop Accessibility Guide.
#
linkText = text.getText(0, -1)
speech.speak(linkText, self.voices[settings.HYPERLINK_VOICE])
else:
speech.speak(rolenames.getSpeechForRoleName(event.source),
self.voices[settings.HYPERLINK_VOICE])
self.updateBraille(event.source)
def onStateChanged(self, event):
"""Called whenever an object's state changes.
Arguments:
- event: the Event
"""
# HTML radio buttons don't automatically become selected when
# they receive focus. The user has to press the space bar on
# them much like checkboxes. But if the user clicks on the
# radio button with the mouse, we'll wind up speaking the
# state twice because of the focus event.
#
if event.type.startswith("object:state-changed:checked") \
and event.source \
and (event.source.getRole() == pyatspi.ROLE_RADIO_BUTTON) \
and (event.detail1 == 1) \
and self.inDocumentContent(event.source) \
and not self.isAriaWidget(event.source) \
and not isinstance(orca_state.lastInputEvent,
input_event.MouseButtonEvent):
self.visualAppearanceChanged(event, event.source)
return
# If an autocomplete appears beneath an entry, we don't want
# to prevent the user from being able to arrow into it.
#
if event.type.startswith("object:state-changed:showing") \
and event.source \
and (event.source.getRole() == pyatspi.ROLE_WINDOW) \
and orca_state.locusOfFocus:
if orca_state.locusOfFocus.getRole() in [pyatspi.ROLE_ENTRY,
pyatspi.ROLE_LIST_ITEM]:
self._autocompleteVisible = event.detail1
# If the autocomplete has just appeared, we want to speak
# its appearance if the user's verbosity level is verbose
# or if the user forced it to appear with (Alt+)Down Arrow.
#
if self._autocompleteVisible:
level = _settingsManager.getSetting('speechVerbosityLevel')
speakIt = level == settings.VERBOSITY_LEVEL_VERBOSE
if not speakIt \
and isinstance(orca_state.lastInputEvent,
input_event.KeyboardEvent):
keyEvent = orca_state.lastNonModifierKeyEvent
speakIt = (keyEvent.event_string == ("Down"))
if speakIt:
speech.speak(rolenames.getSpeechForRoleName(\
event.source, pyatspi.ROLE_AUTOCOMPLETE))
# We care when the document frame changes it's busy state. That
# means it has started/stopped loading content.
#
if event.type.startswith("object:state-changed:busy"):
if event.source \
and (event.source.getRole() == pyatspi.ROLE_DOCUMENT_FRAME):
# If content is changing, trash our saved guessed labels.
#
self._guessedLabels = {}
finishedLoading = False
if orca_state.locusOfFocus \
and (orca_state.locusOfFocus.getRole() \
== pyatspi.ROLE_LIST_ITEM) \
and not self.inDocumentContent(orca_state.locusOfFocus):
# The event is for the changing contents of the help
# frame as the user navigates from topic to topic in
# the list on the left. Ignore this.
#
return
elif event.detail1:
# A detail1=1 means the page has started loading.
#
self._loadingDocumentContent = True
# Translators: this is in reference to loading a web page
# or some other content.
#
message = _("Loading. Please wait.")
elif event.source.name:
# Translators: this is in reference to loading a web page
# or some other content.
#
message = _("Finished loading %s.") % event.source.name
finishedLoading = True
else:
# Translators: this is in reference to loading a web page
# or some other content.
#
message = _("Finished loading.")
finishedLoading = True
self.presentMessage(message)
if finishedLoading:
# Store the document frame otherwise the first time it
# gains focus (e.g. the first time the user arrows off
# of a link into non-focusable text), onStateFocused
# will start chatting unnecessarily.
#
self._currentFrame = event.source
# We first try to figure out where the caret is on
# the newly loaded page. If it is on an editable
# object (e.g., a text entry), then we present just
# that object. Otherwise, we force the caret to the
# top of the page and start a SayAll from that position.
#
[obj, characterOffset] = self.getCaretContext()
atTop = False
if not obj:
self.clearCaretContext()
[obj, characterOffset] = self.getCaretContext()
atTop = True
# If we found nothing, then don't do anything. Otherwise
# determine if we should do a SayAll or not.
#
if not obj:
return
elif not atTop \
and not obj.getState().contains(\
pyatspi.STATE_FOCUSABLE):
self.clearCaretContext()
[obj, characterOffset] = self.getCaretContext()
if not obj:
return
# For braille, we just show the current line
# containing the caret. For speech, however, we
# will start a Say All operation if the caret is
# in an unfocusable area (e.g., it's not in a text
# entry area such as Google's search text entry
# or a link that we just returned to by pressing
# the back button). Otherwise, we'll just speak the
# line that the caret is on.
#
self.updateBraille(obj)
if obj.getState().contains(pyatspi.STATE_FOCUSABLE):
speech.speak(self.speechGenerator.generateSpeech(obj))
elif not script_settings.sayAllOnLoad:
self.speakContents(\
self.getLineContentsAtOffset(obj,
characterOffset))
elif _settingsManager.getSetting('enableSpeech'):
self.sayAll(None)
return
default.Script.onStateChanged(self, event)
def onStateFocused(self, event):
default.Script.onStateChanged(self, event)
if event.source.getRole() == pyatspi.ROLE_DOCUMENT_FRAME and \
event.detail1:
documentFrame = event.source
parent_attribs = self._getAttrDictionary(documentFrame.parent)
parent_tag = parent_attribs.get('tag', '')
if self._loadingDocumentContent or \
documentFrame == self._currentFrame or \
not parent_tag.endswith('browser'):
return
self._currentFrame = documentFrame
self.displayBrailleMessage(documentFrame.name)
speech.stop()
speech.speak(
"%s %s" \
% (documentFrame.name,
rolenames.rolenames[pyatspi.ROLE_PAGE_TAB].speech))
[obj, characterOffset] = self.getCaretContext()
if not obj:
[obj, characterOffset] = self.findNextCaretInOrder()
self.setCaretContext(obj, characterOffset)
if not obj:
return
# When a document tab is tabbed to, we will just present the
# line where the caret currently is.
#
self.presentLine(obj, characterOffset)
def onWindowDeactivated(self, event):
"""Called whenever a toplevel window is deactivated.
Arguments:
- event: the Event
"""
self._objectForFocusGrab = None
default.Script.onWindowDeactivated(self, event)
def handleProgressBarUpdate(self, event, obj):
"""Determine whether this progress bar event should be spoken or not.
For Firefox, we don't want to speak the small "page load" progress
bar. All other Firefox progress bars get passed onto the parent
class for handling.
Arguments:
- event: if not None, the Event that caused this to happen
- obj: the Accessible progress bar object.
"""
rolesList = [pyatspi.ROLE_PROGRESS_BAR, \
pyatspi.ROLE_STATUS_BAR, \
pyatspi.ROLE_FRAME, \
pyatspi.ROLE_APPLICATION]
if not self.utilities.hasMatchingHierarchy(event.source, rolesList):
default.Script.handleProgressBarUpdate(self, event, obj)
def visualAppearanceChanged(self, event, obj):
"""Called when the visual appearance of an object changes. This
method should not be called for objects whose visual appearance
changes solely because of focus -- setLocusOfFocus is used for that.
Instead, it is intended mostly for objects whose notional 'value' has
changed, such as a checkbox changing state, a progress bar advancing,
a slider moving, text inserted, caret moved, etc.
Arguments:
- event: if not None, the Event that caused this to happen
- obj: the Accessible whose visual appearance changed.
"""
if obj.getRole() == pyatspi.ROLE_RADIO_BUTTON \
and self.utilities.isSameObject(orca_state.locusOfFocus, obj):
msg = self.speechGenerator.generateSpeech(obj, alreadyFocused=True)
if self.inDocumentContent(obj):
speech.speak(msg)
self.updateBraille(obj)
return
if (obj.getRole() == pyatspi.ROLE_CHECK_BOX) \
and obj.getState().contains(pyatspi.STATE_FOCUSED):
orca.setLocusOfFocus(event, obj, notifyScript=False)
default.Script.visualAppearanceChanged(self, event, obj)
def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus):
"""Called when the visual object with focus changes.
Arguments:
- event: if not None, the Event that caused the change
- oldLocusOfFocus: Accessible that is the old locus of focus
- newLocusOfFocus: Accessible that is the new locus of focus
"""
# Sometimes we get different accessibles for the same object.
#
if self.utilities.isSameObject(oldLocusOfFocus, newLocusOfFocus):
return
# We always automatically go back to focus tracking mode when
# the focus changes.
#
if self.flatReviewContext:
self.toggleFlatReviewMode()
# Try to handle the case where a spurious focus event was tossed
# at us.
#
if newLocusOfFocus and self.inDocumentContent(newLocusOfFocus):
text = self.utilities.queryNonEmptyText(newLocusOfFocus)
if text:
caretOffset = text.caretOffset
# If the old locusOfFocus was not in the document frame, and
# if the old locusOfFocus's frame is the same as the frame
# containing the new locusOfFocus, we likely just returned
# from a toolbar (find, location, menu bar, etc.). We do
# not want to speak the hierarchy between that toolbar and
# the document frame.
#
if oldLocusOfFocus and \
not self.inDocumentContent(oldLocusOfFocus):
oldFrame = self.utilities.ancestorWithRole(
oldLocusOfFocus, [pyatspi.ROLE_FRAME], [])
newFrame = self.utilities.ancestorWithRole(
newLocusOfFocus, [pyatspi.ROLE_FRAME], [])
if self.utilities.isSameObject(oldFrame, newFrame) or \
newLocusOfFocus.getRole() == pyatspi.ROLE_DIALOG:
self.setCaretPosition(newLocusOfFocus, caretOffset)
self.presentLine(newLocusOfFocus, caretOffset)
return
else:
caretOffset = 0
[obj, characterOffset] = \
self.findFirstCaretContext(newLocusOfFocus, caretOffset)
self.setCaretContext(obj, characterOffset)
else:
# If the newLocusOfFocus is not in document content, trash
# our stored guessed labels. This will hopefully maximize
# performance and accuracy when navigating amongst form
# fields while minimizing the cache size (Gecko creates and
# destroys accessibles so frequently that hashing is of no
# use).
#
self._guessedLabels = {}
# If we've just landed in the Find toolbar, reset
# self.madeFindAnnouncement.
#
if newLocusOfFocus and self.utilities.inFindToolbar(newLocusOfFocus):
self.madeFindAnnouncement = False
# We'll ignore focus changes when the document frame is busy.
# This will keep Orca from chatting too much while a page is
# loading. But we should check to be sure the document frame
# is really busy first and also that the event is not coming
# from an object within a dialog box or alert.
#
documentFrame = self.utilities.documentFrame()
if documentFrame:
self._loadingDocumentContent = \
documentFrame.getState().contains(pyatspi.STATE_BUSY)
if self._loadingDocumentContent and event and event.source:
dialogRoles = [pyatspi.ROLE_DIALOG, pyatspi.ROLE_ALERT]
inDialog = event.source.getRole() in dialogRoles \
or self.utilities.ancestorWithRole(
event.source, dialogRoles, [pyatspi.ROLE_DOCUMENT_FRAME])
if not inDialog:
return
# Don't bother speaking all the information about the HTML
# container - it's duplicated all over the place. So, we
# just speak the role.
#
if newLocusOfFocus \
and newLocusOfFocus.getRole() == pyatspi.ROLE_HTML_CONTAINER:
# We always automatically go back to focus tracking mode when
# the focus changes.
#
if self.flatReviewContext:
self.toggleFlatReviewMode()
self.updateBraille(newLocusOfFocus)
speech.speak(rolenames.getSpeechForRoleName(newLocusOfFocus))
return
default.Script.locusOfFocusChanged(self,
event,
oldLocusOfFocus,
newLocusOfFocus)
def findObjectOnLine(self, obj, offset, contents):
"""Determines if the item described by the object and offset is
in the line contents.
Arguments:
- obj: the Accessible
- offset: the character offset within obj
- contents: a list of (obj, startOffset, endOffset, string) tuples
Returns the index of the item if found; -1 if not found.
"""
if not obj or not contents or not len(contents):
return -1
index = -1
for content in contents:
[candidate, start, end, string] = content
# When we get the line contents, we include a focusable list
# as a list and combo box as a combo box because that is what
# we want to present. However, when we set the caret context,
# we set it to the position (and object) that immediately
# precedes it. Therefore, that's what we need to look at when
# trying to determine our position.
#
if candidate.getRole() in [pyatspi.ROLE_LIST,
pyatspi.ROLE_COMBO_BOX] \
and candidate.getState().contains(pyatspi.STATE_FOCUSABLE) \
and not self.utilities.isSameObject(obj, candidate):
start = self.utilities.characterOffsetInParent(candidate)
end = start + 1
candidate = candidate.parent
if self.utilities.isSameObject(obj, candidate) \
and start <= offset < end:
index = contents.index(content)
break
return index
def _updateLineCache(self, obj, offset):
"""Tries to intelligently update our stored lines. Destroying them if
need be.
Arguments:
- obj: the Accessible
- offset: the character offset within obj
"""
index = self.findObjectOnLine(obj, offset, self.currentLineContents)
if index < 0:
index = self.findObjectOnLine(obj,
offset,
self._previousLineContents)
if index >= 0:
self._nextLineContents = self.currentLineContents
self.currentLineContents = self._previousLineContents
self._previousLineContents = None
else:
index = self.findObjectOnLine(obj,
offset,
self._nextLineContents)
if index >= 0:
self._previousLineContents = self.currentLineContents
self.currentLineContents = self._nextLineContents
self._nextLineContents = None
else:
self._destroyLineCache()
def _destroyLineCache(self):
"""Removes all of the stored lines."""
self._previousLineContents = None
self.currentLineContents = None
self._nextLineContents = None
self.currentAttrs = {}
def presentLine(self, obj, offset):
"""Presents the current line in speech and in braille.
Arguments:
- obj: the Accessible at the caret
- offset: the offset within obj
"""
contents = self.currentLineContents
index = self.findObjectOnLine(obj, offset, contents)
if index < 0:
self.currentLineContents = self.getLineContentsAtOffset(obj,
offset)
if not isinstance(orca_state.lastInputEvent, input_event.BrailleEvent):
self.speakContents(self.currentLineContents)
self.updateBraille(obj)
def updateBraille(self, obj, extraRegion=None):
"""Updates the braille display to show the given object.
Arguments:
- obj: the Accessible
- extra: extra Region to add to the end
"""
if not self.inDocumentContent():
default.Script.updateBraille(self, obj, extraRegion)
return
if not obj:
return
line = self.getNewBrailleLine(clearBraille=True, addLine=True)
# Some text areas have a character offset of -1 when you tab
# into them. In these cases, they show all the text as being
# selected. We don't know quite what to do in that case,
# so we'll just pretend the caret is at the beginning (0).
#
[focusedObj, focusedCharacterOffset] = self.getCaretContext()
# [[[TODO: HACK - WDW when composing e-mail in Thunderbird and
# when entering text in editable text areas, Gecko likes to
# force the last character of a line to be a newline. So,
# we adjust for this because we want to keep meaningful text
# on the display.]]]
#
needToRefresh = False
lineContentsOffset = focusedCharacterOffset
focusedObjText = self.utilities.queryNonEmptyText(focusedObj)
if focusedObjText:
char = focusedObjText.getText(focusedCharacterOffset,
focusedCharacterOffset + 1)
if char == "\n":
lineContentsOffset = max(0, focusedCharacterOffset - 1)
needToRefresh = True
if not self.isNavigableAria(focusedObj):
# Sometimes looking for the first caret context means that we
# are on a child within a non-navigable object (such as the
# text within a page tab). If so, set the focusedObj to the
# parent widget.
#
if not self.isAriaWidget(focusedObj):
focusedObj, focusedCharacterOffset = focusedObj.parent, 0
lineContentsOffset = 0
# Sometimes we just want to present the current object rather
# than the full line. For instance, if we're on a slider we
# should just present that slider. We'll assume we want the
# full line, however.
#
presentOnlyFocusedObj = False
if focusedObj and focusedObj.getRole() == pyatspi.ROLE_SLIDER:
presentOnlyFocusedObj = True
contents = self.currentLineContents
index = self.findObjectOnLine(focusedObj,
max(0, lineContentsOffset),
contents)
if index < 0 or needToRefresh:
contents = self.getLineContentsAtOffset(focusedObj,
max(0, lineContentsOffset))
self.currentLineContents = contents
index = self.findObjectOnLine(focusedObj,
max(0, lineContentsOffset),
contents)
if not len(contents):
return
whitespace = [" ", "\n", self.NO_BREAK_SPACE_CHARACTER]
focusedRegion = None
for i, content in enumerate(contents):
isFocusedObj = (i == index)
[obj, startOffset, endOffset, string] = content
if not obj:
continue
elif presentOnlyFocusedObj and not isFocusedObj:
continue
role = obj.getRole()
if (not len(string) and role != pyatspi.ROLE_PARAGRAPH) \
or self.utilities.isEntry(obj) \
or self.utilities.isPasswordText(obj) \
or role in [pyatspi.ROLE_LINK, pyatspi.ROLE_PUSH_BUTTON]:
[regions, fRegion] = \
self.brailleGenerator.generateBraille(obj)
if isFocusedObj:
focusedRegion = fRegion
else:
regions = [self.getNewBrailleText(obj,
startOffset=startOffset,
endOffset=endOffset)]
if role == pyatspi.ROLE_CAPTION:
regions.append(self.getNewBrailleRegion(
" " + rolenames.getBrailleForRoleName(obj)))
if isFocusedObj:
focusedRegion = regions[0]
# We only want to display the heading role and level if we
# have found the final item in that heading, or if that
# heading contains no children.
#
isLastObject = (i == len(contents) - 1)
if role == pyatspi.ROLE_HEADING \
and (isLastObject or not obj.childCount):
heading = obj
elif isLastObject:
heading = self.utilities.ancestorWithRole(
obj, [pyatspi.ROLE_HEADING], [pyatspi.ROLE_DOCUMENT_FRAME])
else:
heading = None
if heading:
level = self.getHeadingLevel(heading)
# Translators: the 'h' below represents a heading level
# attribute for content that you might find in something
# such as HTML content (e.g.,
). The translated form
# is meant to be a single character followed by a numeric
# heading level, where the single character is to indicate
# 'heading'.
#
headingString = _("h%d" % level)
if not string.endswith(" "):
headingString = " " + headingString
if not isLastObject:
headingString += " "
regions.append(self.getNewBrailleRegion((headingString)))
# Add whitespace if we need it. [[[TODO: JD - But should we be
# doing this in the braille generators rather than here??]]]
#
if len(line.regions) \
and regions[0].string and line.regions[-1].string \
and not regions[0].string[0] in whitespace \
and not line.regions[-1].string[-1] in whitespace:
# There is nothing separating the previous braille region from
# this one. We might or might not want to add some whitespace
# for readability.
#
lastObj = contents[i - 1][0]
# If we have two of the same braille class, or if the previous
# region is a component or a generic region, or an image link,
# we should add some space.
#
if line.regions[-1].__class__ == regions[0].__class__ \
or line.regions[-1].__class__ in [braille.Component,
braille.Region] \
or lastObj.getRole() == pyatspi.ROLE_IMAGE \
or obj.getRole() == pyatspi.ROLE_IMAGE:
self.addToLineAsBrailleRegion(" ", line)
# The above check will catch table cells with uniform
# contents and form fields -- and do so more efficiently
# than walking up the hierarchy. But if we have a cell
# with text next to a cell with a link.... Ditto for
# sections on the same line.
#
else:
layoutRoles = [pyatspi.ROLE_TABLE_CELL,
pyatspi.ROLE_SECTION,
pyatspi.ROLE_LIST_ITEM]
if role in layoutRoles:
acc1 = obj
else:
acc1 = self.utilities.ancestorWithRole(
obj, layoutRoles, [pyatspi.ROLE_DOCUMENT_FRAME])
if acc1:
if lastObj.getRole() == acc1.getRole():
acc2 = lastObj
else:
acc2 = self.utilities.ancestorWithRole(
lastObj,
layoutRoles,
[pyatspi.ROLE_DOCUMENT_FRAME])
if not self.utilities.isSameObject(acc1, acc2):
self.addToLineAsBrailleRegion(" ", line)
self.addBrailleRegionsToLine(regions, line)
if isLastObject:
line.regions[-1].string = line.regions[-1].string.rstrip(" ")
# If we're inside of a combo box, we only want to display
# the selected menu item.
#
if obj.getRole() == pyatspi.ROLE_MENU_ITEM \
and obj.getState().contains(pyatspi.STATE_FOCUSED):
break
if extraRegion:
self.addBrailleRegionToLine(extraRegion, line)
self.setBrailleFocus(focusedRegion, getLinkMask=False)
self.refreshBraille(panToCursor=True, getLinkMask=False)
def sayCharacter(self, obj):
"""Speaks the character at the current caret position."""
# We need to handle HTML content differently because of the
# EMBEDDED_OBJECT_CHARACTER model of Gecko. For all other
# things, however, we can defer to the default scripts.
#
if not self.inDocumentContent() or self.utilities.isEntry(obj):
default.Script.sayCharacter(self, obj)
return
[obj, characterOffset] = self.getCaretContext()
text = self.utilities.queryNonEmptyText(obj)
if text:
# If the caret is at the end of text and we're not in an
# entry, something bad is going on, so decrement the offset
# before speaking the character.
#
string = text.getText(0, -1)
if characterOffset >= len(string) \
and not obj.getState().contains(pyatspi.STATE_EDITABLE):
print "YIKES in Gecko.sayCharacter!"
characterOffset -= 1
if characterOffset >= 0:
self.speakCharacterAtOffset(obj, characterOffset)
def sayWord(self, obj):
"""Speaks the word at the current caret position."""
# We need to handle HTML content differently because of the
# EMBEDDED_OBJECT_CHARACTER model of Gecko. For all other
# things, however, we can defer to the default scripts.
#
if not self.inDocumentContent():
default.Script.sayWord(self, obj)
return
[obj, characterOffset] = self.getCaretContext()
text = self.utilities.queryNonEmptyText(obj)
if text:
# [[[TODO: WDW - the caret might be at the end of the text.
# Not quite sure what to do in this case. What we'll do here
# is just speak the previous word. But...maybe we want to
# make sure we say something like "end of line" or move the
# caret context to the beginning of the next word via
# a call to goNextWord.]]]
#
string = text.getText(0, -1)
if characterOffset >= len(string) \
and not obj.getState().contains(pyatspi.STATE_EDITABLE):
print "YIKES in Gecko.sayWord!"
characterOffset -= 1
# Ideally in an entry we would just let default.sayWord() handle
# things. That fails to work when navigating backwords by word.
# Because getUtterancesFromContents() now uses the speech_generator
# with entries, we need to handle word navigation in entries here.
#
wordContents = self.getWordContentsAtOffset(obj, characterOffset)
[textObj, startOffset, endOffset, word] = wordContents[0]
self.speakMisspelledIndicator(textObj, startOffset)
if not self.utilities.isEntry(textObj):
self.speakContents(wordContents)
else:
word = self.utilities.substring(textObj, startOffset, endOffset)
speech.speak([word], self.getACSS(textObj, word))
def sayLine(self, obj):
"""Speaks the line at the current caret position."""
# We need to handle HTML content differently because of the
# EMBEDDED_OBJECT_CHARACTER model of Gecko. For all other
# things, however, we can defer to the default scripts.
#
if not self.inDocumentContent() or self.utilities.isEntry(obj):
default.Script.sayLine(self, obj)
return
[obj, characterOffset] = self.getCaretContext()
text = self.utilities.queryNonEmptyText(obj)
if text:
# [[[TODO: WDW - the caret might be at the end of the text.
# Not quite sure what to do in this case. What we'll do here
# is just speak the current line. But...maybe we want to
# make sure we say something like "end of line" or move the
# caret context to the beginning of the next line via
# a call to goNextLine.]]]
#
string = text.getText(0, -1)
if characterOffset >= len(string) \
and not obj.getState().contains(pyatspi.STATE_EDITABLE):
print "YIKES in Gecko.sayLine!"
characterOffset -= 1
self.speakContents(self.getLineContentsAtOffset(obj, characterOffset))
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 self.isAriaWidget(orca_state.locusOfFocus) \
or not self.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 self.isAriaWidget(orca_state.locusOfFocus) \
or not self.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
####################################################################
# #
# Methods for debugging. #
# #
####################################################################
def outlineExtents(self, obj, startOffset, endOffset):
"""Draws an outline around the given text for the object or the entire
object if it has no text. This is for debug purposes only.
Arguments:
-obj: the object
-startOffset: character offset to start at
-endOffset: character offset just after last character to end at
"""
[x, y, width, height] = self.getExtents(obj, startOffset, endOffset)
self.drawOutline(x, y, width, height)
def dumpInfo(self, obj):
"""Dumps the parental hierachy info of obj to stdout."""
if obj.parent:
self.dumpInfo(obj.parent)
print "---"
text = self.utilities.queryNonEmptyText(obj)
if text and obj.getRole() != pyatspi.ROLE_DOCUMENT_FRAME:
string = text.getText(0, -1)
else:
string = ""
print obj, obj.name, obj.getRole(), \
obj.accessible.getIndexInParent(), string
offset = self.utilities.characterOffsetInParent(obj)
if offset >= 0:
print " offset =", offset
def getDocumentContents(self):
"""Returns an ordered list where each element is composed of
an [obj, startOffset, endOffset] tuple. The list is created
via an in-order traversal of the document contents starting at
the current caret context (or the beginning of the document if
there is no caret context).
WARNING: THIS TRAVERSES A LARGE PART OF THE DOCUMENT AND IS
INTENDED PRIMARILY FOR DEBUGGING PURPOSES ONLY."""
contents = []
lastObj = None
lastExtents = None
self.clearCaretContext()
[obj, characterOffset] = self.getCaretContext()
while obj:
if True or obj.getState().contains(pyatspi.STATE_SHOWING):
if self.utilities.queryNonEmptyText(obj):
# Check for text being on a different line. Gecko
# gives us odd character extents sometimes, so we
# defensively ignore those.
#
characterExtents = self.getExtents(
obj, characterOffset, characterOffset + 1)
if characterExtents != (0, 0, 0, 0):
if lastExtents \
and not self.onSameLine(lastExtents,
characterExtents):
contents.append([None, -1, -1])
lastExtents = characterExtents
# Check to see if we've moved across objects or are
# still on the same object. If we've moved, we want
# to add another context. If we're still on the same
# object, we just want to update the end offset.
#
if (len(contents) == 0) or (obj != lastObj):
contents.append([obj,
characterOffset,
characterOffset + 1])
else:
[currentObj, startOffset, endOffset] = contents[-1]
if characterOffset == endOffset:
contents[-1] = [currentObj, # obj
startOffset, # startOffset
endOffset + 1] # endOffset
else:
contents.append([obj,
characterOffset,
characterOffset + 1])
else:
# Some objects present text and/or something visual
# (e.g., a checkbox), so we want to track it.
#
contents.append([obj, -1, -1])
lastObj = obj
[obj, characterOffset] = self.findNextCaretInOrder(obj,
characterOffset)
return contents
def getDocumentString(self):
"""Trivial debug utility to stringify the document contents
showing on the screen."""
contents = ""
lastObj = None
lastCharacterExtents = None
[obj, characterOffset] = self.getCaretContext()
while obj:
if obj and obj.getState().contains(pyatspi.STATE_SHOWING):
characterExtents = self.getExtents(
obj, characterOffset, characterOffset + 1)
if lastObj and (lastObj != obj):
if obj.getRole() == pyatspi.ROLE_LIST_ITEM:
contents += "\n"
if lastObj.getRole() == pyatspi.ROLE_LINK:
contents += ">"
elif (lastCharacterExtents[1] < characterExtents[1]):
contents += "\n"
elif obj.getRole() == pyatspi.ROLE_TABLE_CELL:
parent = obj.parent
index = self.utilities.cellIndex(obj)
if parent.queryTable().getColumnAtIndex(index) != 0:
contents += " "
elif obj.getRole() == pyatspi.ROLE_LINK:
contents += "<"
contents += self.getCharacterAtOffset(obj, characterOffset)
lastObj = obj
lastCharacterExtents = characterExtents
[obj, characterOffset] = self.findNextCaretInOrder(obj,
characterOffset)
if lastObj and lastObj.getRole() == pyatspi.ROLE_LINK:
contents += ">"
return contents
def dumpContents(self, inputEvent, contents=None):
"""Dumps the document frame content to stdout.
Arguments:
-inputEvent: the input event that caused this to be called
-contents: an ordered list of [obj, startOffset, endOffset] tuples
"""
if not contents:
contents = self.getDocumentContents()
string = ""
extents = None
for content in contents:
[obj, startOffset, endOffset] = content
if obj:
extents = self.getBoundary(
self.getExtents(obj, startOffset, endOffset),
extents)
text = self.utilities.queryNonEmptyText(obj)
if text:
string += "[%s] text='%s' " % (obj.getRole(),
text.getText(startOffset,
endOffset))
else:
string += "[%s] name='%s' " % (obj.getRole(), obj.name)
else:
string += "\nNEWLINE\n"
print "==========================="
print string
self.drawOutline(extents[0], extents[1], extents[2], extents[3])
####################################################################
# #
# Utility Methods #
# #
####################################################################
def inDocumentContent(self, obj=None):
"""Returns True if the given object (defaults to the current
locus of focus is in the document content).
"""
if not obj:
obj = orca_state.locusOfFocus
try:
return self.generatorCache['inDocumentContent'][obj]
except:
pass
result = False
while obj:
role = obj.getRole()
if role == pyatspi.ROLE_DOCUMENT_FRAME \
or role == pyatspi.ROLE_EMBEDDED:
result = True
break
else:
obj = obj.parent
if not self.generatorCache.has_key('inDocumentContent'):
self.generatorCache['inDocumentContent'] = {}
if obj:
self.generatorCache['inDocumentContent'][obj] = result
return result
def useCaretNavigationModel(self, keyboardEvent):
"""Returns True if we should do our own caret navigation.
"""
if not script_settings.controlCaretNavigation:
return False
if not self.inDocumentContent():
return False
if not self.isNavigableAria(orca_state.locusOfFocus):
return False
if keyboardEvent.event_string in ["Page_Up", "Page_Down"]:
return False
if self._loadingDocumentContent:
return False
weHandleIt = True
obj = orca_state.locusOfFocus
if self.utilities.isEntry(obj):
text = obj.queryText()
length = text.characterCount
caretOffset = text.caretOffset
singleLine = obj.getState().contains(
pyatspi.STATE_SINGLE_LINE)
# Single line entries have an additional newline character
# at the end.
#
newLineAdjustment = int(not singleLine)
# Home and End should not be overridden if we're in an
# entry.
#
if keyboardEvent.event_string in ["Home", "End"]:
return False
# We want to use our caret navigation model in an entry if
# there's nothing in the entry, we're at the beginning of
# the entry and press Left or Up, or we're at the end of the
# entry and press Right or Down.
#
if length == 0 \
or ((length == 1) and (text.getText(0, -1) == "\n")):
weHandleIt = True
elif caretOffset <= 0:
weHandleIt = keyboardEvent.event_string \
in ["Up", "Left"]
elif caretOffset >= length - newLineAdjustment \
and not self._autocompleteVisible:
weHandleIt = keyboardEvent.event_string \
in ["Down", "Right"]
else:
weHandleIt = False
if singleLine and not weHandleIt \
and not self._autocompleteVisible:
weHandleIt = keyboardEvent.event_string in ["Up", "Down"]
elif keyboardEvent.modifiers & settings.ALT_MODIFIER_MASK:
# Alt+Down Arrow is the Firefox command to expand/collapse the
# *currently focused* combo box. When Orca is controlling the
# caret, it is possible to navigate into a combo box *without
# giving focus to that combo box*. Under these circumstances,
# the menu item has focus. Because the user knows that he/she
# is on a combo box, he/she expects to be able to use Alt+Down
# Arrow to expand the combo box. Therefore, if a menu item has
# focus and Alt+Down Arrow is pressed, we will handle it by
# giving the combo box focus and expanding it as the user
# expects. We also want to avoid grabbing focus on a combo box.
# Therefore, if the caret is immediately before a combo box,
# we'll hand it the same way.
#
if keyboardEvent.event_string == "Down":
[obj, offset] = self.getCaretContext()
index = self.getChildIndex(obj, offset)
if index >= 0:
weHandleIt = \
obj[index].getRole() == pyatspi.ROLE_COMBO_BOX
if not weHandleIt:
weHandleIt = obj.getRole() == pyatspi.ROLE_MENU_ITEM
elif obj and (obj.getRole() == pyatspi.ROLE_COMBO_BOX):
# We'll let Firefox handle the navigation of combo boxes.
#
weHandleIt = keyboardEvent.event_string in ["Left", "Right"]
elif obj and (obj.getRole() in [pyatspi.ROLE_MENU_ITEM,
pyatspi.ROLE_LIST_ITEM]):
# We'll let Firefox handle the navigation of combo boxes and
# lists in forms.
#
weHandleIt = not obj.getState().contains(pyatspi.STATE_FOCUSED)
elif obj and (obj.getRole() == pyatspi.ROLE_LIST):
# We'll let Firefox handle the navigation of lists in forms.
#
weHandleIt = not obj.getState().contains(pyatspi.STATE_FOCUSABLE)
return weHandleIt
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.
"""
letThemDoItEditableRoles = [pyatspi.ROLE_ENTRY,
pyatspi.ROLE_TEXT,
pyatspi.ROLE_PASSWORD_TEXT]
letThemDoItSelectionRoles = [pyatspi.ROLE_LIST,
pyatspi.ROLE_LIST_ITEM,
pyatspi.ROLE_MENU_ITEM]
if not self.structuralNavigation.enabled:
return False
if not self.isNavigableAria(orca_state.locusOfFocus):
return False
if self._loadingDocumentContent:
return False
# If the Orca_Modifier key was pressed, we're handling it.
#
if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
mods = orca_state.lastInputEvent.modifiers
isOrcaKey = mods & settings.ORCA_MODIFIER_MASK
if isOrcaKey:
return True
obj = orca_state.locusOfFocus
while obj:
if obj.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
# Don't use the structural navivation model if the
# user is editing the document.
return not obj.getState().contains(pyatspi.STATE_EDITABLE)
elif obj.getRole() in letThemDoItEditableRoles:
return not obj.getState().contains(pyatspi.STATE_EDITABLE)
elif obj.getRole() in letThemDoItSelectionRoles:
return not obj.getState().contains(pyatspi.STATE_FOCUSED)
elif obj.getRole() == pyatspi.ROLE_COMBO_BOX:
return False
else:
obj = obj.parent
return False
def isNavigableAria(self, obj):
"""Returns True if the object being examined is an ARIA widget where
we want to provide Orca keyboard navigation. Returning False
indicates that we want Firefox to handle key commands.
"""
try:
state = obj.getState()
except (LookupError, RuntimeError):
debug.println(debug.LEVEL_SEVERE,
"isNavigableAria() - obj no longer exists")
return True
except:
pass
else:
# If the current object isn't even showing, we don't want to hand
# this off to Firefox's native caret navigation because who knows
# where we'll wind up....
#
if state.contains(pyatspi.STATE_SHOWING):
return True
# Sometimes the child of an ARIA widget claims focus. It may lack
# the attributes we're looking for. Therefore, if obj is not an
# ARIA widget, we'll also consider the parent's attributes.
#
attrs = self._getAttrDictionary(obj)
if obj and not self.isAriaWidget(obj):
attrs.update(self._getAttrDictionary(obj.parent))
try:
# ARIA landmark widgets
if set(attrs['xml-roles'].split()).intersection(\
set(settings.ariaLandmarks)):
return True
# ARIA live region
elif 'container-live' in attrs:
return True
# Don't treat links as ARIA widgets. And we should be able to
# escape/exit ARIA entries just like we do HTML entries (How
# is the user supposed to know which he/she happens to be in?)
#
elif obj.getRole() in [pyatspi.ROLE_ENTRY,
pyatspi.ROLE_LINK,
pyatspi.ROLE_ALERT,
pyatspi.ROLE_PARAGRAPH,
pyatspi.ROLE_SECTION]:
return obj.parent.getRole() not in [pyatspi.ROLE_COMBO_BOX,
pyatspi.ROLE_PAGE_TAB]
# All other ARIA widgets we will assume are navigable if
# they are not focused.
#
else:
return not obj.getState().contains(pyatspi.STATE_FOCUSABLE)
except (KeyError, TypeError):
return True
def isAriaWidget(self, obj=None):
"""Returns True if the object being examined is an ARIA widget.
Arguments:
- obj: The accessible object of interest. If None, the
locusOfFocus is examined.
"""
try:
return self.generatorCache['isAria'][obj]
except:
pass
obj = obj or orca_state.locusOfFocus
attrs = self._getAttrDictionary(obj)
if not self.generatorCache.has_key('isAria'):
self.generatorCache['isAria'] = {}
self.generatorCache['isAria'][obj] = \
('xml-roles' in attrs and 'live' not in attrs)
return self.generatorCache['isAria'][obj]
def _getAttrDictionary(self, obj):
if not obj:
return {}
return dict([attr.split(':', 1) for attr in obj.getAttributes()])
def handleAsLiveRegion(self, event):
"""Returns True if the given event (object:children-changed, object:
text-insert only) should be considered a live region event"""
# We will try to eliminate objects that cannot be considered live
# regions. We will handle everything else as a live region. We
# will do the cheap tests first
if self._loadingDocumentContent \
or not _settingsManager.getSetting('inferLiveRegions'):
return False
# Ideally, we would like to do a inDocumentContent() call to filter out
# events that are not in the document. Unfortunately, this is an
# expensive call. Instead we will do some heuristics to filter out
# chrome events with the least amount of IPC as possible.
# event.type specific checks
if event.type.startswith('object:children-changed'):
if event.type.endswith(':system'):
# This will filter out list items that are not of interest and
# events from other tabs.
stateset = event.any_data.getState()
if stateset.contains(pyatspi.STATE_SELECTABLE) \
or not stateset.contains(pyatspi.STATE_VISIBLE):
return False
# Now we need to look at the object attributes
attrs = self._getAttrDictionary(event.any_data)
# Good live region markup
if 'container-live' in attrs:
return True
# We see this one with the URL bar opening (sometimes)
if 'tag' in attrs and attrs['tag'] == 'xul:richlistbox':
return False
if 'xml-roles' in attrs:
# This eliminates all ARIA widgets that are not
# considered live
attrList = attrs['xml-roles'].split()
if not 'alert' in attrList \
and not 'tooltip' in attrList:
return False
# Only present tooltips when user wants them presented
elif 'tooltip' in attrList \
and not _settingsManager.getSetting('presentToolTips'):
return False
else:
# Some alerts have been seen without the :system postfix.
# We will take care of them separately.
attrs = self._getAttrDictionary(event.any_data)
if 'xml-roles' in attrs \
and 'alert' in attrs['xml-roles'].split():
return True
else:
return False
elif event.type.startswith('object:text-changed:insert:system'):
# Live regions will not be focusable.
# Filter out events from hidden tabs (not VISIBLE)
stateset = event.source.getState()
if stateset.contains(pyatspi.STATE_FOCUSABLE) \
or stateset.contains(pyatspi.STATE_SELECTABLE) \
or not stateset.contains(pyatspi.STATE_VISIBLE):
return False
attrs = self._getAttrDictionary(event.source)
# Good live region markup
if 'container-live' in attrs:
return True
# This might be too restrictive but we need it to filter
# out URLs that are displayed when the location list opens.
if 'tag' in attrs \
and attrs['tag'] == 'xul:description' \
or attrs['tag'] == 'xul:label':
return False
# This eliminates all ARIA widgets that are not considered live
if 'xml-roles' in attrs:
return False
elif event.type.startswith('object:text-changed:insert'):
# We do this since we sometimes get text inserted events
# without the ":system" suffix for live regions (see bug
# 550873). [[[WDW - this probably could be conflated into
# the block above, making that block check for just
# object:text-changed:insert" events, but I wanted to be a
# little more conservative since the live region stuff was
# done long ago and I've forgotten many of the details.]]]
#
stateset = event.source.getState()
if stateset.contains(pyatspi.STATE_FOCUSABLE) \
or stateset.contains(pyatspi.STATE_SELECTABLE) \
or not stateset.contains(pyatspi.STATE_VISIBLE):
return False
attrs = self._getAttrDictionary(event.source)
return 'container-live' in attrs \
or event.source.getRole() == pyatspi.ROLE_ALERT
else:
return False
# This last filter gets rid of some events that come in after
# window:activate event. They are usually areas of a page that
# are built dynamically.
if time.time() - self._loadingDocumentTime > 2.0:
return True
else:
return False
def getChildIndex(self, obj, characterOffset):
"""Given an object that implements accessible text, determine
the index of the child that is represented by an
EMBEDDED_OBJECT_CHARACTER at characterOffset in the object's
accessible text."""
try:
hypertext = obj.queryHypertext()
except NotImplementedError:
index = -1
else:
index = hypertext.getLinkIndex(characterOffset)
return index
def getExtents(self, obj, startOffset, endOffset):
"""Returns [x, y, width, height] of the text at the given offsets
if the object implements accessible text, or just the extents of
the object if it doesn't implement accessible text.
"""
if not obj:
return [0, 0, 0, 0]
# The menu items that are children of combo boxes have unique
# extents based on their physical position, even though they are
# not showing. Therefore, if the object in question is a menu
# item, get the object extents rather than the range extents for
# the text. Similarly, if it's a menu in a combo box, get the
# extents of the combo box.
#
text = self.utilities.queryNonEmptyText(obj)
if text and obj.getRole() != pyatspi.ROLE_MENU_ITEM:
extents = text.getRangeExtents(startOffset, endOffset, 0)
elif obj.getRole() == pyatspi.ROLE_MENU \
and obj.parent.getRole() == pyatspi.ROLE_COMBO_BOX:
ext = obj.parent.queryComponent().getExtents(0)
extents = [ext.x, ext.y, ext.width, ext.height]
else:
ext = obj.queryComponent().getExtents(0)
extents = [ext.x, ext.y, ext.width, ext.height]
return extents
def getBoundary(self, a, b):
"""Returns the smallest [x, y, width, height] that encompasses
both extents a and b.
Arguments:
-a: [x, y, width, height]
-b: [x, y, width, height]
"""
if not a:
return b
if not b:
return a
smallestX1 = min(a[0], b[0])
smallestY1 = min(a[1], b[1])
largestX2 = max(a[0] + a[2], b[0] + b[2])
largestY2 = max(a[1] + a[3], b[1] + b[3])
return [smallestX1,
smallestY1,
largestX2 - smallestX1,
largestY2 - smallestY1]
def onSameLine(self, a, b, pixelDelta=5):
"""Determine if extents a and b are on the same line.
Arguments:
-a: [x, y, width, height]
-b: [x, y, width, height]
Returns True if a and b are on the same line.
"""
# If a and b are identical, by definition they are on the same line.
#
if a == b:
return True
# For now, we'll just take a look at the bottom of the area.
# The code after this takes the whole extents into account,
# but that logic has issues in the case where we have
# something very tall next to lots of shorter lines (e.g., an
# image with lots of text to the left or right of it. The
# number 11 here represents something that seems to work well
# with superscripts and subscripts on a line as well as pages
# with smaller fonts on them, such as craig's list.
#
if abs(a[1] - b[1]) > 11:
return False
# If there's an overlap of 1 pixel or less, they are on different
# lines. Keep in mind "lowest" and "highest" mean visually on the
# screen, but that the value is the y coordinate.
#
highestBottom = min(a[1] + a[3], b[1] + b[3])
lowestTop = max(a[1], b[1])
if lowestTop >= highestBottom - 1:
return False
return True
# If we do overlap, lets see how much. We'll require a 25% overlap
# for now...
#
#if lowestTop < highestBottom:
# overlapAmount = highestBottom - lowestTop
# shortestHeight = min(a[3], b[3])
# return ((1.0 * overlapAmount) / shortestHeight) > 0.25
#else:
# return False
def isLabellingContents(self, obj, contents):
"""Given and obj and a list of [obj, startOffset, endOffset] tuples,
determine if obj is labelling anything in the tuples.
Returns the object being labelled, or None.
"""
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 getAutocompleteEntry(self, obj):
"""Returns the ROLE_ENTRY object of a ROLE_AUTOCOMPLETE object or
None if the entry cannot be found.
"""
for child in obj:
if child and (child.getRole() == pyatspi.ROLE_ENTRY):
return child
return None
def getCellCoordinates(self, obj):
"""Returns the [row, col] of a ROLE_TABLE_CELL or [0, 0]
if the coordinates cannot be found.
"""
if obj.getRole() != pyatspi.ROLE_TABLE_CELL:
obj = self.utilities.ancestorWithRole(
obj, [pyatspi.ROLE_TABLE_CELL], [pyatspi.ROLE_DOCUMENT_FRAME])
parentTable = self.utilities.ancestorWithRole(
obj, [pyatspi.ROLE_TABLE], [pyatspi.ROLE_DOCUMENT_FRAME])
try:
table = parentTable.queryTable()
except:
pass
else:
index = self.utilities.cellIndex(obj)
row = table.getRowAtIndex(index)
col = table.getColumnAtIndex(index)
return [row, col]
return [0, 0]
def isBlankCell(self, obj):
"""Returns True if the table cell is empty or consists of a single
non-breaking space.
Arguments:
- obj: the table cell to examime
"""
text = self.utilities.displayedText(obj)
if text and text != u'\u00A0':
return False
else:
for child in obj:
if child.getRole() == pyatspi.ROLE_LINK:
return False
return True
def getLinkBasename(self, obj):
"""Returns the relevant information from the URI. The idea is
to attempt to strip off all prefix and suffix, much like the
basename command in a shell."""
basename = None
try:
hyperlink = obj.queryHyperlink()
except:
pass
else:
uri = hyperlink.getURI(0)
if uri and len(uri):
# Sometimes the URI is an expression that includes a URL.
# Currently that can be found at the bottom of safeway.com.
# It can also be seen in the backwards.html test file.
#
expression = uri.split(',')
if len(expression) > 1:
for item in expression:
if item.find('://') >=0:
if not item[0].isalnum():
item = item[1:-1]
if not item[-1].isalnum():
item = item[0:-2]
uri = item
break
# We're assuming that there IS a base name to be had.
# What if there's not? See backwards.html.
#
uri = uri.split('://')[-1]
# Get the last thing after all the /'s, unless it ends
# in a /. If it ends in a /, we'll look to the stuff
# before the ending /.
#
if uri[-1] == "/":
basename = uri[0:-1]
basename = basename.split('/')[-1]
elif not uri.count("/"):
basename = uri
else:
basename = uri.split('/')[-1]
if basename.startswith("index"):
basename = uri.split('/')[-2]
# Now, try to strip off the suffixes.
#
basename = basename.split('.')[0]
basename = basename.split('?')[0]
basename = basename.split('#')[0]
return basename
def isFormField(self, obj):
"""Returns True if the given object is a field inside of a form."""
if not obj or not self.inDocumentContent(obj):
return False
formRoles = [pyatspi.ROLE_CHECK_BOX,
pyatspi.ROLE_RADIO_BUTTON,
pyatspi.ROLE_COMBO_BOX,
pyatspi.ROLE_DOCUMENT_FRAME,
pyatspi.ROLE_LIST,
pyatspi.ROLE_ENTRY,
pyatspi.ROLE_PASSWORD_TEXT,
pyatspi.ROLE_PUSH_BUTTON]
state = obj.getState()
isField = obj.getRole() in formRoles \
and state.contains(pyatspi.STATE_FOCUSABLE) \
and state.contains(pyatspi.STATE_SENSITIVE)
if obj.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
isField = isField and state.contains(pyatspi.STATE_EDITABLE)
return isField
def isUselessObject(self, obj):
"""Returns true if the given object is an obj that doesn't
have any meaning associated with it and it is not inside a
link."""
if not obj:
return True
useless = False
textObj = self.utilities.queryNonEmptyText(obj)
if not textObj and obj.getRole() == pyatspi.ROLE_PARAGRAPH:
# Under these circumstances, this object is useless even
# if it is the child of a link.
#
return True
elif obj.getRole() in [pyatspi.ROLE_IMAGE, \
pyatspi.ROLE_TABLE_CELL, \
pyatspi.ROLE_SECTION]:
text = self.utilities.displayedText(obj)
if (not text) or (len(text) == 0):
text = self.utilities.displayedLabel(obj)
if (not text) or (len(text) == 0):
useless = True
if useless:
link = self.utilities.ancestorWithRole(
obj, [pyatspi.ROLE_LINK], [pyatspi.ROLE_DOCUMENT_FRAME])
if link:
if obj.getRole() == pyatspi.ROLE_IMAGE:
# If this object had alternative text and/or a title,
# we wouldn't be here. We need to determine if this
# image is indeed worth presenting and not a duplicate
# piece of information. See Facebook's timeline and/or
# bug 584540.
#
for child in obj.parent:
if self.utilities.displayedText(child):
# Some other sibling is presenting information.
# We'll treat this image as useless.
#
break
else:
# No other siblings are presenting information.
#
if obj.parent.getRole() == pyatspi.ROLE_LINK:
if not link.name:
# If no siblings are presenting information,
# but the link had a name, then we'd know we
# had text along with the image(s). Given the
# lack of name, we'll treat the first image as
# the useful one and ignore the rest.
#
useless = obj.getIndexInParent() > 0
else:
# The image must be in a paragraph or section or
# heading or something else that might result in
# it being on its own line.
#
textObj = \
self.utilities.queryNonEmptyText(obj.parent)
if textObj:
text = textObj.getText(0, -1).decode("UTF-8")
text = text.replace(\
self.EMBEDDED_OBJECT_CHARACTER, "").strip()
if not text:
# There's no other text on this line inside
# of this link. We don't want to skip over
# this line, so we'll treat the first image
# as useful.
#
useless = obj.getIndexInParent() > 0
else:
useless = False
return useless
def pursueForFlatReview(self, obj):
"""Determines if we should look any further at the object
for flat review."""
# It should be enough to check for STATE_SHOWING, but Gecko seems
# to reverse STATE_SHOWING and STATE_VISIBLE, exposing STATE_SHOWING
# for objects which are offscreen. So, we'll check for both. See
# bug #542833. [[[TODO - JD: We're putting this check in just this
# script for now to be on the safe side. Consider for the default
# script as well?]]]
#
try:
state = obj.getState()
except:
debug.printException(debug.LEVEL_WARNING)
return False
else:
return state.contains(pyatspi.STATE_SHOWING) \
and state.contains(pyatspi.STATE_VISIBLE)
def getHeadingLevel(self, obj):
"""Determines the heading level of the given object. A value
of 0 means there is no heading level."""
level = 0
if obj is None:
return level
if obj.getRole() == pyatspi.ROLE_HEADING:
attributes = obj.getAttributes()
if attributes is None:
return level
for attribute in attributes:
if attribute.startswith("level:"):
level = int(attribute.split(":")[1])
break
return level
def getTopOfFile(self):
"""Returns the object and first caret offset at the top of the
document frame."""
documentFrame = self.utilities.documentFrame()
[obj, offset] = self.findFirstCaretContext(documentFrame, 0)
return [obj, offset]
def getBottomOfFile(self):
"""Returns the object and last caret offset at the bottom of the
document frame."""
documentFrame = self.utilities.documentFrame()
text = self.utilities.queryNonEmptyText(documentFrame)
if text:
char = text.getText(text.characterCount - 1, text.characterCount)
if char != self.EMBEDDED_OBJECT_CHARACTER:
return [documentFrame, text.characterCount - 1]
obj = self.getLastObject(documentFrame)
offset = 0
# If the last object is a link, it may be more efficient to check
# for text that follows.
#
if obj.getRole() == pyatspi.ROLE_LINK:
text = self.utilities.queryNonEmptyText(obj.parent)
if text:
char = text.getText(text.characterCount - 1,
text.characterCount)
if char != self.EMBEDDED_OBJECT_CHARACTER:
return [obj.parent, text.characterCount - 1]
# obj should now be the very last item in the entire document frame
# and not have children of its own. Therefore, it should have text.
# If it doesn't, we don't want to be here.
#
text = self.utilities.queryNonEmptyText(obj)
if text:
offset = text.characterCount - 1
else:
obj = self.findPreviousObject(obj, documentFrame)
while obj:
[lastObj, lastOffset] = self.findNextCaretInOrder(obj, offset)
if not lastObj \
or self.utilities.isSameObject(lastObj, obj) \
and (lastOffset == offset):
break
[obj, offset] = [lastObj, lastOffset]
return [obj, offset]
def getLastObject(self, documentFrame):
"""Returns the last object in the document frame"""
try:
lastChild = documentFrame[documentFrame.childCount - 1]
except:
lastChild = documentFrame
while lastChild:
lastObj = self.findNextObject(lastChild, documentFrame)
if lastObj:
lastChild = lastObj
else:
break
return lastChild
def getNextCellInfo(self, cell, direction):
"""Given a cell from which to start and a direction in which to
search locates the next cell and returns it, along with its
text, extents (as a tuple), and whether or not the cell contents
consist of a form field.
Arguments
- cell: the table cell from which to start
- direction: a string which can be one of four options: 'left',
'right', 'up', 'down'
Returns [nextCell, text, extents, isField]
"""
newCell = None
text = ""
extents = (0, 0, 0, 0)
isField = False
parentTable = self.utilities.ancestorWithRole(
cell, [pyatspi.ROLE_TABLE], [pyatspi.ROLE_DOCUMENT_FRAME])
if not cell or cell.getRole() != pyatspi.ROLE_TABLE_CELL \
or not parentTable:
return [newCell, text, extents, isField]
[row, col] = self.getCellCoordinates(cell)
table = parentTable.queryTable()
rowspan = table.getRowExtentAt(row, col)
colspan = table.getColumnExtentAt(row, col)
nextCell = None
if direction == "left" and col > 0:
nextCell = (row, col - 1)
elif direction == "right" \
and (col + colspan <= table.nColumns - 1):
nextCell = (row, col + colspan)
elif direction == "up" and row > 0:
nextCell = (row - 1, col)
elif direction == "down" \
and (row + rowspan <= table.nRows - 1):
nextCell = (row + rowspan, col)
if nextCell:
newCell = table.getAccessibleAt(nextCell[0], nextCell[1])
objects = self.getObjectsFromEOCs(newCell, 0)
if len(objects):
extents = self.getExtents(objects[0][0],
objects[0][1],
objects[0][2])
for obj in objects:
if obj[0].getRole() == pyatspi.ROLE_IMAGE:
text += obj[0].name
elif not self.isFormField(obj[0]):
text += obj[3]
else:
isField = True
text = ""
exts = obj[0].queryComponent().getExtents(0)
extents = [exts.x, exts.y, exts.width, exts.height]
break
return [newCell, text, extents, isField]
def getObjectsFromEOCs(self, obj, offset, boundary=None):
"""Expands the current object replacing EMBEDDED_OBJECT_CHARACTERS
with [obj, startOffset, endOffset, string] tuples.
Arguments
- obj: the object whose EOCs we need to expand into tuples
- offset: the character offset after which
- boundary: the pyatspi text boundary type
Returns a list of object tuples.
"""
if not obj:
return []
elif boundary and obj.getRole() == pyatspi.ROLE_TABLE:
# If this is a table, move to the first cell -- or the caption,
# if present.
# [[[TODOS - JD:
# 1) It might be nice to announce the fact that we've just
# found a table, what its dimensions are, etc.
# 2) It seems that down arrow moves us to the table, but up
# arrow moves us to the last row. Possible side effect
# of our existing caret browsing implementation??]]]
# 3) Figure out why the heck the table of contents for at
# least some Yelp content consists of a table whose sole
# child is a list!!!
if obj[0] and obj[0].getRole() in [pyatspi.ROLE_CAPTION,
pyatspi.ROLE_LIST]:
obj = obj[0]
else:
obj = obj.queryTable().getAccessibleAt(0, 0)
if not obj:
# Yelp (or perhaps the work-in-progress a11y patch) seems
# to be guilty of this. Although that may have been the
# table of contents thing (see #3 above).
#
#print "getObjectsFromEOCs - in Table, missing an accessible"
debug.printStack(debug.LEVEL_WARNING)
return []
objects = []
text = self.utilities.queryNonEmptyText(obj)
if text:
if boundary:
[string, start, end] = \
text.getTextAfterOffset(offset, boundary)
else:
start = offset
end = text.characterCount
string = text.getText(start, end)
else:
string = ""
start = 0
end = 1
unicodeText = string.decode("UTF-8")
objects.append([obj, start, end, unicodeText])
pattern = re.compile(self.EMBEDDED_OBJECT_CHARACTER)
matches = re.finditer(pattern, unicodeText)
offset = 0
for m in matches:
# Adjust the last object's endOffset to the last character
# before the EOC.
#
childOffset = m.start(0) + start
lastObj = objects[-1]
lastObj[2] = childOffset
if lastObj[1] == lastObj[2]:
# A zero-length object is an indication of something
# whose sole contents was an EOC. Delete it from the
# list.
#
objects.pop()
else:
# Adjust the string to reflect just this segment.
#
lastObj[3] = unicodeText[offset:m.start(0)]
offset = m.start(0) + 1
# Recursively tack on the child's objects.
#
childIndex = self.getChildIndex(obj, childOffset)
child = obj[childIndex]
objects.extend(self.getObjectsFromEOCs(child, 0, boundary))
# Tack on the remainder of the original object, if any.
#
if end > childOffset + 1:
restOfText = unicodeText[offset:len(unicodeText)]
objects.append([obj, childOffset + 1, end, restOfText])
if obj.getRole() in [pyatspi.ROLE_IMAGE, pyatspi.ROLE_TABLE]:
# Imagemaps that don't have alternative text won't implement
# the text interface, but they will have children (essentially
# EOCs) that we need to get. The same is true for tables.
#
toAdd = []
for child in obj:
toAdd.extend(self.getObjectsFromEOCs(child, 0, boundary))
if len(toAdd):
if self.utilities.isSameObject(objects[-1][0], obj):
objects.pop()
objects.extend(toAdd)
return objects
def getMeaningfulObjectsFromLine(self, line):
"""Attempts to strip a list of (obj, start, end) tuples into one
that contains only meaningful objects."""
if not line or not len(line):
return []
lineContents = []
for item in line:
role = item[0].getRole()
# If it's labelling something on this line, don't move to
# it.
#
if role == pyatspi.ROLE_LABEL \
and self.isLabellingContents(item[0], line):
continue
# Rather than do a brute force label guess, we'll focus on
# entries as they are the most common and their label is
# likely on this line. The functional label may be made up
# of several objects, so we'll examine the strings of what
# we've got and pop off the ones that match.
#
elif self.utilities.isEntry(item[0]):
labelGuess = self.guessLabelFromLine(item[0])
index = len(lineContents) - 1
while labelGuess and index >= 0:
prevItem = lineContents[index]
prevText = self.utilities.queryNonEmptyText(prevItem[0])
if prevText:
string = prevText.getText(prevItem[1], prevItem[2])
if labelGuess.endswith(string):
lineContents.pop()
length = len(labelGuess) - len(string)
labelGuess = labelGuess[0:length]
else:
break
index -= 1
else:
text = self.utilities.queryNonEmptyText(item[0])
if text:
string = text.getText(item[1], item[2]).decode("UTF-8")
if not len(string.strip()):
continue
lineContents.append(item)
return lineContents
def getPageSummary(self, obj):
"""Returns the quantity of headings, forms, tables, visited links,
and unvisited links on the page containing obj.
"""
if _settingsManager.getSetting('useCollection'):
try:
summary = self._collectionPageSummary()
except:
debug.printException(debug.LEVEL_SEVERE)
summary = self._iterativePageSummary(obj)
else:
summary = self._iterativePageSummary(obj)
return summary
def _collectionPageSummary(self):
"""Uses the Collection interface to get the quantity of headings,
forms, tables, visited and unvisited links.
"""
docframe = self.utilities.documentFrame()
col = docframe.queryCollection()
# We will initialize these after the queryCollection() call in case
# Collection is not supported
#
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.utilities.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]
def _iterativePageSummary(self, obj):
"""Reads the quantity of headings, forms, tables, visited and
unvisited links.
"""
headings = 0
forms = 0
tables = 0
vlinks = 0
uvlinks = 0
percentRead = None
nodetotal = 0
obj_index = None
currentobj = obj
# Start at the first object after document frame.
#
obj = self.utilities.documentFrame()[0]
while obj:
nodetotal += 1
if obj == currentobj:
obj_index = nodetotal
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.utilities.isLayoutOnly(obj):
tables += 1
elif role == pyatspi.ROLE_LINK:
if obj.getState().contains(pyatspi.STATE_VISITED):
vlinks += 1
else:
uvlinks += 1
obj = self.findNextObject(obj)
# Calculate the percentage of the document that has been read.
#
if obj_index:
percentRead = int(obj_index*100/nodetotal)
return [headings, forms, tables, vlinks, uvlinks, percentRead]
def guessLabelFromLine(self, obj):
"""Attempts to guess what the label of an unlabeled form control
might be by looking at surrounding contents from the same line.
Arguments
- obj: the form field about which to take a guess
Returns the text which we think might be the label or None if we
give up.
"""
# Based on Tom Brunet's comments on how Home Page Reader
# approached the task of guessing labels. Please see:
# https://bugzilla.mozilla.org/show_bug.cgi?id=376481#c15
#
# 1. Text/img that precedes control in same item.
# 2. Text/img that follows control in same item (nothing between
# end of text and item)
#
# Reverse this order for radio buttons and check boxes
#
lineContents = self.currentLineContents
ourIndex = self.findObjectOnLine(obj, 0, lineContents)
if ourIndex < 0:
lineContents = self.getLineContentsAtOffset(obj, 0)
ourIndex = self.findObjectOnLine(obj, 0, lineContents)
thisObj = lineContents[ourIndex]
objExtents = self.getExtents(thisObj[0], thisObj[1], thisObj[2])
leftGuess = ""
extents = objExtents
for i in range (ourIndex - 1, -1, -1):
candidate, start, end, string = lineContents[i]
if self.isFormField(candidate):
break
prevExtents = self.getExtents(candidate, start, end)
if -1 <= extents[0] - (prevExtents[0] + prevExtents[2]) < 75:
# The candidate might be an image with alternative text.
#
string = string or candidate.name
leftGuess = string + leftGuess
extents = prevExtents
# Normally we prefer what's on the left given a choice. Reasons
# to prefer what's on the right include looking at a radio button
# or a checkbox. [[[TODO - JD: Language direction should also be
# taken into account.]]]
#
preferRight = obj.getRole() in [pyatspi.ROLE_CHECK_BOX,
pyatspi.ROLE_RADIO_BUTTON]
# Sometimes we don't want the text on the right -- at least not
# until we are able to present labels on the right after the
# object we believe they are labeling, rather than before.
#
preventRight = obj.getRole() == pyatspi.ROLE_COMBO_BOX
rightGuess = ""
extents = objExtents
if not preventRight and (preferRight or not leftGuess):
for i in range (ourIndex + 1, len(lineContents)):
candidate, start, end, string = lineContents[i]
# If we're looking on the right and find text, and then
# find another nearby form field, the text we've found
# might be the label for that field rather than for this
# one. We'll assume that for now and bail under these
# conditions.
#
if self.isFormField(candidate):
if not preferRight:
rightGuess = ""
break
nextExtents = self.getExtents(candidate, start, end)
if -1 <= nextExtents[0] - (extents[0] + extents[2]) < 75:
# The candidate might be an image with alternative text.
#
string = string or candidate.name
rightGuess += string
extents = nextExtents
guess = rightGuess or leftGuess
return guess.strip()
def guessLabelFromOtherLines(self, obj):
"""Attempts to guess what the label of an unlabeled form control
might be by looking at nearby contents from neighboring lines.
Arguments
- obj: the form field about which to take a guess
Returns the text which we think might be the label or None if we
give up.
"""
# Based on Tom Brunet's comments on how Home Page Reader
# approached the task of guessing labels. Please see:
# https://bugzilla.mozilla.org/show_bug.cgi?id=376481#c15
#
guess = None
extents = obj.queryComponent().getExtents(0)
objExtents = \
[extents.x, extents.y, extents.width, extents.height]
index = self.findObjectOnLine(obj, 0, self.currentLineContents)
if index > 0 and self._previousLineContents:
prevLineContents = self._previousLineContents
prevObj = prevLineContents[0][0]
prevOffset = prevLineContents[0][1]
else:
[prevObj, prevOffset] = self.findPreviousLine(obj, 0, False)
prevLineContents = self.getLineContentsAtOffset(prevObj,
prevOffset)
# The labels for combo boxes won't be found below the combo box
# because expanding the combo box will cover up the label. Labels
# for lists probably won't be below the list either.
#
if obj.getRole() in [pyatspi.ROLE_COMBO_BOX,
pyatspi.ROLE_MENU,
pyatspi.ROLE_MENU_ITEM,
pyatspi.ROLE_LIST,
pyatspi.ROLE_LIST_ITEM]:
[nextObj, nextOffset] = [None, 0]
nextLineContents = []
else:
nextLineContents = self._nextLineContents
if index > 0 and nextLineContents:
nextObj = nextLineContents[0][0]
nextOffset = nextLineContents[0][1]
else:
[nextObj, nextOffset] = self.findNextLine(obj, 0, False)
nextLineContents = self.getLineContentsAtOffset(nextObj,
nextOffset)
above = None
lastExtents = (0, 0, 0, 0)
for content in prevLineContents:
aboveExtents = self.getExtents(content[0], content[1], content[2])
# [[[TODO: Grayed out buttons don't pass the isFormField()
# test because they are neither focusable nor showing -- and
# thus something we don't want to navigate to via structural
# navigation. We may need to rethink our definition of
# isFormField(). In the meantime, let's not used grayed out
# buttons as labels. As an example, see the Search entry on
# live.gnome.org. We want to ignore menu items as well.]]]
#
aboveIsFormField = self.isFormField(content[0]) \
or content[0].getRole() in [pyatspi.ROLE_PUSH_BUTTON,
pyatspi.ROLE_MENU_ITEM,
pyatspi.ROLE_LIST]
# If the horizontal starting point of the object is the
# same as the horizontal starting point of the text
# above it, the text above it is probably serving as the
# label. We'll allow for a 2 pixel difference. If that
# fails, and the text above starts within 50 pixels to
# the left and ends somewhere above or beyond the current
# form field, we'll give it the benefit of the doubt.
# For an example of the latter case, see Bugzilla's Advanced
# search page, Bug Changes section.
#
if not above:
if (objExtents != aboveExtents) and not aboveIsFormField:
xDiff = objExtents[0] - aboveExtents[0]
guessThis = (0 <= abs(xDiff) <= 2)
if not guessThis and (0 <= xDiff <= 50):
guessThis = \
(aboveExtents[0] + aboveExtents[2] > objExtents[0])
if guessThis:
above = content[0]
guessAbove = content[3]
else:
# The "label" might be comprised of several objects (e.g.
# text with links).
#
lastEnd = lastExtents[0] + lastExtents[2]
if lastEnd - aboveExtents[0] < 10 and not aboveIsFormField:
guessAbove += content[3]
else:
break
lastExtents = aboveExtents
below = None
lastExtents = (0, 0, 0, 0)
for content in nextLineContents:
belowExtents = self.getExtents(content[0], content[1], content[2])
# [[[TODO: Grayed out buttons don't pass the isFormField()
# test because they are neither focusable nor showing -- and
# thus something we don't want to navigate to via structural
# navigation. We may need to rethink our definition of
# isFormField(). In the meantime, let's not used grayed out
# buttons as labels. As an example, see the Search entry on
# live.gnome.org. We want to ignore menu items as well.]]]
#
belowIsFormField = self.isFormField(content[0]) \
or content[0].getRole() in [pyatspi.ROLE_PUSH_BUTTON,
pyatspi.ROLE_MENU_ITEM,
pyatspi.ROLE_LIST]
# If the horizontal starting point of the object is the
# same as the horizontal starting point of the text
# below it, the text below it is probably serving as the
# label. We'll allow for a 2 pixel difference.
#
if not below:
if (objExtents != belowExtents) and not belowIsFormField \
and 0 <= abs(objExtents[0] - belowExtents[0]) <= 2:
below = content[0]
guessBelow = content[3]
else:
# The "label" might be comprised of several objects (e.g.
# text with links).
#
lastEnd = lastExtents[0] + lastExtents[2]
if lastEnd - belowExtents[0] < 10 and not belowIsFormField:
guessBelow += content[3]
else:
break
lastExtents = belowExtents
if above:
if not below:
guess = guessAbove
else:
# We'll guess the nearest text.
#
bottomOfAbove = aboveExtents[1] + aboveExtents[3]
topOfBelow = belowExtents[1]
bottomOfObj = objExtents[1] + objExtents[3]
topOfObject = objExtents[1]
aboveProximity = topOfObject - bottomOfAbove
belowProximity = topOfBelow - bottomOfObj
if aboveProximity <= belowProximity \
or belowProximity < 0:
guess = guessAbove
else:
guess = guessBelow
elif below:
guess = guessBelow
return guess
def guessLabelFromTable(self, obj):
"""Attempts to guess what the label of an unlabeled form control
might be by looking at surrounding table cells.
Arguments
- obj: the form field about which to take a guess
Returns the text which we think might be the label or None if we
give up.
"""
# Based on Tom Brunet's comments on how Home Page Reader
# approached the task of guessing labels. Please see:
# https://bugzilla.mozilla.org/show_bug.cgi?id=376481#c15
#
# "3. Text/img that precedes control in previous item/cell
# not another control in that item)..."
#
# 4. Text/img in cell above without other controls in this
# cell above."
#
# If that fails, we might as well look to the cell below. If the
# text is immediately below the entry and nothing else looks like
# a label, that text might be it. Given both text above and below
# the control, the most likely label is probably the text that is
# vertically nearest it. This theory will, of course, require
# testing "in the wild."
#
guess = None
# If we're not the sole occupant of a table cell, we're either
# not in a table at all or are in a more complex layout table
# than this approach can handle.
#
containingCell = self.utilities.ancestorWithRole(
obj, [pyatspi.ROLE_TABLE_CELL], [pyatspi.ROLE_DOCUMENT_FRAME])
if not containingCell or containingCell.childCount > 1:
return guess
extents = obj.queryComponent().getExtents(0)
objExtents = [extents.x, extents.y, extents.width, extents.height]
[cellLeft, leftText, leftExtents, leftIsField] = \
self.getNextCellInfo(containingCell, "left")
[cellRight, rightText, rightExtents, rightIsField] = \
self.getNextCellInfo(containingCell, "right")
[cellAbove, aboveText, aboveExtents, aboveIsField] = \
self.getNextCellInfo(containingCell, "up")
# The labels for combo boxes won't be found below the combo box
# because expanding the combo box will cover up the label. Labels
# for lists probably won't be below the list either.
#
if obj.getRole() in [pyatspi.ROLE_COMBO_BOX,
pyatspi.ROLE_MENU,
pyatspi.ROLE_MENU_ITEM,
pyatspi.ROLE_LIST,
pyatspi.ROLE_LIST_ITEM]:
[cellBelow, belowText, belowExtents, belowIsField] = \
[None, "", (0, 0, 0, 0), False]
else:
[cellBelow, belowText, belowExtents, belowIsField] = \
self.getNextCellInfo(containingCell, "down")
if rightText:
# The object's horizontal position plus its width tells us
# where the text on the right can begin. For now, define
# "immediately after" as within 50 pixels.
#
canStartAt = objExtents[0] + objExtents[2]
rightCloseEnough = rightExtents[0] - canStartAt <= 50
if leftText and not leftIsField:
guess = leftText
elif rightText and rightCloseEnough and not rightIsField:
guess = rightText
elif aboveText and not aboveIsField:
if not belowText or belowIsField:
guess = aboveText
else:
# We'll guess the nearest text.
#
bottomOfAbove = aboveExtents[1] + aboveExtents[3]
topOfBelow = belowExtents[1]
bottomOfObj = objExtents[1] + objExtents[3]
topOfObject = objExtents[1]
aboveProximity = topOfObject - bottomOfAbove
belowProximity = topOfBelow - bottomOfObj
if aboveProximity <= belowProximity:
guess = aboveText
else:
guess = belowText
elif belowText and not belowIsField:
guess = belowText
elif aboveIsField:
# Given the lack of potential labels and the fact that
# there's a form field immediately above us, there's
# a reasonable chance that we're in a series of form
# fields arranged grid-style. It's even more likely
# if the form fields above us are all of the same type
# and size (say, within 1 pixel).
#
nextCell = containingCell
while nextCell:
[nextCell, text, extents, isField] = \
self.getNextCellInfo(nextCell, "up")
if nextCell:
dWidth = abs(objExtents[2] - extents[2])
dHeight = abs(objExtents[3] - extents[3])
if (dWidth > 1 or dHeight > 1):
if not isField:
[row, col] = self.getCellCoordinates(nextCell)
if row == 0:
guess = text
break
return guess
def guessTheLabel(self, obj, focusedOnly=True):
"""Attempts to guess what the label of an unlabeled form control
might be.
Arguments
- obj: the form field about which to take a guess
- focusedOnly: If True, only take guesses about the form field
with focus.
Returns the text which we think might be the label or None if we
give up.
"""
# The initial stab at this is based on Tom Brunet's comments
# on how Home Page Reader approached this task. His comments
# can be found at the RFE for Mozilla to do this work for us:
# https://bugzilla.mozilla.org/show_bug.cgi?id=376481#c15
# N.B. If you see a comment in quotes, it's taken directly from
# Tom.
#
guess = None
# If we're not in the document frame, we don't want to be guessing.
# We also don't want to be guessing if the item doesn't have focus.
#
isFocused = obj.getState().contains(pyatspi.STATE_FOCUSED)
if not self.inDocumentContent() \
or (focusedOnly and not isFocused) \
or self.isAriaWidget(obj):
return guess
# Maybe we've already made a guess and saved it.
#
for field, label in self._guessedLabels.items():
if self.utilities.isSameObject(field, obj):
return label
parent = obj.parent
text = self.utilities.queryNonEmptyText(parent)
# Because the guesswork is based upon spatial relations, if we're
# in a list, look from the perspective of the first list item rather
# than from the list as a whole.
#
if obj.getRole() == pyatspi.ROLE_LIST:
obj = obj[0]
guess = self.guessLabelFromLine(obj)
# print "guess from line: ", guess
if not guess:
# Maybe it's in a table cell.
#
guess = self.guessLabelFromTable(obj)
# print "guess from table: ", guess
if not guess:
# Maybe the text is above or below us, but not in a table
# cell -- or in a table cell which contains multiple items
# and/or line breaks.
#
if parent.getRole() != pyatspi.ROLE_TABLE_CELL \
or parent.childCount > 1 \
or (text and text.getText(0, -1).find("\n") >= 0):
guess = self.guessLabelFromOtherLines(obj)
#print "guess from other lines: ", guess
if not guess:
# We've pretty much run out of options. From Tom's overview
# of the approach for all controls:
# "... 4. title attribute."
# The title attribute seems to be exposed as the name.
#
guess = obj.name
#print "Guessing the name: ", guess
if obj.parent.getRole() == pyatspi.ROLE_LIST:
obj = obj.parent
guess = guess.strip()
self._guessedLabels[obj] = guess
return guess.strip()
####################################################################
# #
# Methods to find previous and next objects. #
# #
####################################################################
def findFirstCaretContext(self, obj, characterOffset):
"""Given an object and a character offset, find the first
[obj, characterOffset] that is actually presenting something
on the display. The reason we do this is that the
[obj, characterOffset] passed in may actually be pointing
to an embedded object character. In those cases, we dig
into the hierarchy to find the 'real' thing.
Arguments:
-obj: an accessible object
-characterOffset: the offset of the character where to start
looking for real rext
Returns [obj, characterOffset] that points to real content.
"""
text = self.utilities.queryNonEmptyText(obj)
if text:
unicodeText = self.utilities.unicodeText(obj)
if characterOffset >= len(unicodeText):
if not self.utilities.isEntry(obj):
return [obj, -1]
else:
# We're at the end of an entry. If we return -1,
# and then set the caretContext accordingly,
# findNextCaretInOrder() will think we're at the
# beginning and we'll never escape this entry.
#
return [obj, characterOffset]
character = text.getText(characterOffset,
characterOffset + 1).decode("UTF-8")
if character == self.EMBEDDED_OBJECT_CHARACTER:
if obj.childCount <= 0:
return self.findFirstCaretContext(obj, characterOffset + 1)
try:
childIndex = self.getChildIndex(obj, characterOffset)
return self.findFirstCaretContext(obj[childIndex], 0)
except:
return [obj, -1]
else:
# [[[TODO: WDW - HACK because Gecko currently exposes
# whitespace from the raw HTML to us. We can infer this
# by seeing if the extents are nil. If so, we skip to
# the next character.]]]
#
extents = self.getExtents(obj,
characterOffset,
characterOffset + 1)
if (extents == (0, 0, 0, 0)) \
and ((characterOffset + 1) < len(unicodeText)):
return self.findFirstCaretContext(obj, characterOffset + 1)
else:
return [obj, characterOffset]
elif obj.getRole() == pyatspi.ROLE_TABLE:
if obj[0] and obj[0].getRole() in [pyatspi.ROLE_CAPTION,
pyatspi.ROLE_LIST]:
obj = obj[0]
else:
obj = obj.queryTable().getAccessibleAt(0, 0)
return self.findFirstCaretContext(obj, 0)
else:
return [obj, -1]
def findNextCaretInOrder(self, obj=None,
startOffset=-1,
includeNonText=True):
"""Given an object at a character offset, return the next
caret context following an in-order traversal rule.
Arguments:
- root: the Accessible to start at. If None, starts at the
document frame.
- startOffset: character position in the object text field
(if it exists) to start at. Defaults to -1, which means
start at the beginning - that is, the next character is the
first character in the object.
- includeNonText: If False, only land on objects that support the
accessible text interface; otherwise, include logical leaf
nodes like check boxes, combo boxes, etc.
Returns [obj, characterOffset] or [None, -1]
"""
if not obj:
obj = self.utilities.documentFrame()
if not obj or not self.inDocumentContent(obj):
return [None, -1]
if obj.getRole() == pyatspi.ROLE_INVALID:
debug.println(debug.LEVEL_SEVERE, \
"findNextCaretInOrder: object is invalid")
return [None, -1]
# We do not want to descend objects of certain role types.
#
doNotDescend = obj.getState().contains(pyatspi.STATE_FOCUSABLE) \
and obj.getRole() in [pyatspi.ROLE_COMBO_BOX,
pyatspi.ROLE_LIST]
text = self.utilities.queryNonEmptyText(obj)
if text:
unicodeText = self.utilities.unicodeText(obj)
# Delete the final space character if we find it. Otherwise,
# we'll arrow to it. (We can't just strip the string otherwise
# we skip over blank lines that one could otherwise arrow to.)
#
if len(unicodeText) > 1 and unicodeText[-1] == " ":
unicodeText = unicodeText[0:len(unicodeText) - 1]
nextOffset = startOffset + 1
while 0 <= nextOffset < len(unicodeText):
if unicodeText[nextOffset] != self.EMBEDDED_OBJECT_CHARACTER:
return [obj, nextOffset]
elif obj.childCount:
child = obj[self.getChildIndex(obj, nextOffset)]
if child:
return self.findNextCaretInOrder(child,
-1,
includeNonText)
else:
nextOffset += 1
else:
nextOffset += 1
# If this is a list or combo box in an HTML form, we don't want
# to place the caret inside the list, but rather treat the list
# as a single object. Otherwise, if it has children, look there.
#
elif obj.childCount and obj[0] and not doNotDescend:
try:
return self.findNextCaretInOrder(obj[0],
-1,
includeNonText)
except:
debug.printException(debug.LEVEL_SEVERE)
elif includeNonText and (startOffset < 0) \
and (not self.utilities.isLayoutOnly(obj)):
extents = obj.queryComponent().getExtents(0)
if (extents.width != 0) and (extents.height != 0):
return [obj, 0]
# If we're here, we need to start looking up the tree,
# going no higher than the document frame, of course.
#
documentFrame = self.utilities.documentFrame()
if self.utilities.isSameObject(obj, documentFrame):
return [None, -1]
while obj.parent and obj != obj.parent:
characterOffsetInParent = \
self.utilities.characterOffsetInParent(obj)
if characterOffsetInParent >= 0:
return self.findNextCaretInOrder(obj.parent,
characterOffsetInParent,
includeNonText)
else:
index = obj.getIndexInParent() + 1
if index < obj.parent.childCount:
try:
return self.findNextCaretInOrder(
obj.parent[index],
-1,
includeNonText)
except:
debug.printException(debug.LEVEL_SEVERE)
obj = obj.parent
return [None, -1]
def findPreviousCaretInOrder(self,
obj=None,
startOffset=-1,
includeNonText=True):
"""Given an object an a character offset, return the previous
caret context following an in order traversal rule.
Arguments:
- root: the Accessible to start at. If None, starts at the
document frame.
- startOffset: character position in the object text field
(if it exists) to start at. Defaults to -1, which means
start at the end - that is, the previous character is the
last character of the object.
Returns [obj, characterOffset] or [None, -1]
"""
if not obj:
obj = self.utilities.documentFrame()
if not obj or not self.inDocumentContent(obj):
return [None, -1]
if obj.getRole() == pyatspi.ROLE_INVALID:
debug.println(debug.LEVEL_SEVERE, \
"findPreviousCaretInOrder: object is invalid")
return [None, -1]
# We do not want to descend objects of certain role types.
#
doNotDescend = obj.getState().contains(pyatspi.STATE_FOCUSABLE) \
and obj.getRole() in [pyatspi.ROLE_COMBO_BOX,
pyatspi.ROLE_LIST]
text = self.utilities.queryNonEmptyText(obj)
if text:
unicodeText = self.utilities.unicodeText(obj)
# Delete the final space character if we find it. Otherwise,
# we'll arrow to it. (We can't just strip the string otherwise
# we skip over blank lines that one could otherwise arrow to.)
#
if len(unicodeText) > 1 and unicodeText[-1] == " ":
unicodeText = unicodeText[0:len(unicodeText) - 1]
if (startOffset == -1) or (startOffset > len(unicodeText)):
startOffset = len(unicodeText)
previousOffset = startOffset - 1
while previousOffset >= 0:
if unicodeText[previousOffset] \
!= self.EMBEDDED_OBJECT_CHARACTER:
return [obj, previousOffset]
elif obj.childCount:
child = obj[self.getChildIndex(obj, previousOffset)]
if child:
return self.findPreviousCaretInOrder(child,
-1,
includeNonText)
else:
previousOffset -= 1
else:
previousOffset -= 1
# If this is a list or combo box in an HTML form, we don't want
# to place the caret inside the list, but rather treat the list
# as a single object. Otherwise, if it has children, look there.
#
elif obj.childCount and obj[obj.childCount - 1] and not doNotDescend:
try:
return self.findPreviousCaretInOrder(
obj[obj.childCount - 1],
-1,
includeNonText)
except:
debug.printException(debug.LEVEL_SEVERE)
elif includeNonText and (startOffset < 0) \
and (not self.utilities.isLayoutOnly(obj)):
extents = obj.queryComponent().getExtents(0)
if (extents.width != 0) and (extents.height != 0):
return [obj, 0]
# If we're here, we need to start looking up the tree,
# going no higher than the document frame, of course.
#
documentFrame = self.utilities.documentFrame()
if self.utilities.isSameObject(obj, documentFrame):
return [None, -1]
while obj.parent and obj != obj.parent:
characterOffsetInParent = \
self.utilities.characterOffsetInParent(obj)
if characterOffsetInParent >= 0:
return self.findPreviousCaretInOrder(obj.parent,
characterOffsetInParent,
includeNonText)
else:
index = obj.getIndexInParent() - 1
if index >= 0:
try:
return self.findPreviousCaretInOrder(
obj.parent[index],
-1,
includeNonText)
except:
debug.printException(debug.LEVEL_SEVERE)
obj = obj.parent
return [None, -1]
def findPreviousObject(self, obj, documentFrame):
"""Finds the object prior to this one, where the tree we're
dealing with is a DOM and 'prior' means the previous object
in a linear presentation sense.
Arguments:
-obj: the object where to start.
"""
previousObj = None
characterOffset = 0
# If the object is the document frame, the previous object is
# the one that follows us relative to our offset.
#
if self.utilities.isSameObject(obj, documentFrame):
[obj, characterOffset] = self.getCaretContext()
if not obj:
return None
index = obj.getIndexInParent() - 1
if (index < 0):
if not self.utilities.isSameObject(obj, documentFrame):
previousObj = obj.parent
else:
# We're likely at the very end of the document
# frame.
previousObj = self.getLastObject(documentFrame)
else:
# [[[TODO: HACK - WDW defensive programming because Gecko
# ally hierarchies are not always working. Objects say
# they have children, but these children don't exist when
# we go to get them. So...we'll just keep going backwards
# until we find a real child that we can work with.]]]
#
while not isinstance(previousObj,
pyatspi.Accessibility.Accessible) \
and index >= 0:
previousObj = obj.parent[index]
index -= 1
# Now that we're at a child we can work with, we need to
# look at it further. It could be the root of a hierarchy.
# In that case, the last child in this hierarchy is what
# we want. So, we dive down the 'right hand side' of the
# tree to get there.
#
# [[[TODO: HACK - WDW we need to be defensive because of
# Gecko's broken a11y hierarchies, so we make this much
# more complex than it really has to be.]]]
#
if not previousObj:
if not self.utilities.isSameObject(obj, documentFrame):
previousObj = obj.parent
else:
previousObj = obj
role = previousObj.getRole()
if role == pyatspi.ROLE_MENU_ITEM:
return previousObj.parent.parent
elif role == pyatspi.ROLE_LIST_ITEM:
parent = previousObj.parent
if parent.getState().contains(pyatspi.STATE_FOCUSABLE) \
and not self.isAriaWidget(parent):
return parent
while previousObj.childCount:
role = previousObj.getRole()
state = previousObj.getState()
if role in [pyatspi.ROLE_COMBO_BOX, pyatspi.ROLE_MENU]:
break
elif role == pyatspi.ROLE_LIST \
and state.contains(pyatspi.STATE_FOCUSABLE) \
and not self.isAriaWidget(previousObj):
break
elif previousObj.childCount > 1000:
break
index = previousObj.childCount - 1
while index >= 0:
child = previousObj[index]
childOffset = self.utilities.characterOffsetInParent(child)
if isinstance(child, pyatspi.Accessibility.Accessible) \
and not (self.utilities.isSameObject(
previousObj, documentFrame) \
and childOffset > characterOffset):
previousObj = child
break
else:
index -= 1
if index < 0:
break
if self.utilities.isSameObject(previousObj, documentFrame):
previousObj = None
return previousObj
def findNextObject(self, obj, documentFrame):
"""Finds the object after to this one, where the tree we're
dealing with is a DOM and 'next' means the next object
in a linear presentation sense.
Arguments:
-obj: the object where to start.
"""
nextObj = None
characterOffset = 0
# If the object is the document frame, the next object is
# the one that follows us relative to our offset.
#
if self.utilities.isSameObject(obj, documentFrame):
[obj, characterOffset] = self.getCaretContext()
if not obj:
return None
# If the object has children, we'll choose the first one,
# unless it's a combo box or a focusable HTML list.
#
# [[[TODO: HACK - WDW Gecko's broken hierarchies make this
# a bit of a challenge.]]]
#
role = obj.getRole()
if role in [pyatspi.ROLE_COMBO_BOX, pyatspi.ROLE_MENU]:
descend = False
elif role == pyatspi.ROLE_LIST \
and obj.getState().contains(pyatspi.STATE_FOCUSABLE) \
and not self.isAriaWidget(obj):
descend = False
elif obj.childCount > 1000:
descend = False
else:
descend = True
index = 0
while descend and index < obj.childCount:
child = obj[index]
# bandaid for Gecko broken hierarchy
if child is None:
index += 1
continue
childOffset = self.utilities.characterOffsetInParent(child)
if isinstance(child, pyatspi.Accessibility.Accessible) \
and not (self.utilities.isSameObject(obj, documentFrame) \
and childOffset < characterOffset):
nextObj = child
break
else:
index += 1
# Otherwise, we'll look to the next sibling.
#
# [[[TODO: HACK - WDW Gecko's broken hierarchies make this
# a bit of a challenge.]]]
#
if not nextObj:
index = obj.getIndexInParent() + 1
while index < obj.parent.childCount:
child = obj.parent[index]
if isinstance(child, pyatspi.Accessibility.Accessible):
nextObj = child
break
else:
index += 1
# If there is no next sibling, we'll move upwards.
#
candidate = obj
while not nextObj:
# Go up until we find a parent that might have a sibling to
# the right for us.
#
while candidate and candidate.parent \
and candidate.getIndexInParent() >= \
candidate.parent.childCount - 1 \
and not self.utilities.isSameObject(candidate, documentFrame):
candidate = candidate.parent
# Now...let's get the sibling.
#
# [[[TODO: HACK - WDW Gecko's broken hierarchies make this
# a bit of a challenge.]]]
#
if not self.utilities.isSameObject(candidate, documentFrame):
index = candidate.getIndexInParent() + 1
while index < candidate.parent.childCount:
child = candidate.parent[index]
if isinstance(child, pyatspi.Accessibility.Accessible):
nextObj = child
break
else:
index += 1
# We've exhausted trying to get all the children, but
# Gecko's broken hierarchy has failed us for all of
# them. So, we need to go higher.
#
candidate = candidate.parent
else:
break
return nextObj
####################################################################
# #
# Methods to get information about current object. #
# #
####################################################################
def clearCaretContext(self):
"""Deletes all knowledge of a character context for the current
document frame."""
documentFrame = self.utilities.documentFrame()
self._destroyLineCache()
try:
del self._documentFrameCaretContext[hash(documentFrame)]
except:
pass
def setCaretContext(self, obj=None, characterOffset=-1):
"""Sets the caret context for the current document frame."""
# We keep a context for each page tab shown.
# [[[TODO: WDW - probably should figure out how to destroy
# these contexts when a tab is killed.]]]
#
documentFrame = self.utilities.documentFrame()
if not documentFrame:
return
self._documentFrameCaretContext[hash(documentFrame)] = \
[obj, characterOffset]
self._updateLineCache(obj, characterOffset)
def getTextLineAtCaret(self, obj, offset=None):
"""Gets the portion of the line of text where the caret (or optional
offset) is. This is an override to accomodate the intricities of our
caret navigation management and to deal with bogus line information
being returned by Gecko when using getTextAtOffset.
Argument:
- obj: an Accessible object that implements the AccessibleText
interface
- offset: an optional caret offset to use.
Returns the [string, caretOffset, startOffset] for the line of text
where the caret is.
"""
# We'll let the default script handle entries and other entry-like
# things (e.g. the text portion of a dojo spin button).
#
if not self.inDocumentContent(obj) \
or self.utilities.isEntry(obj) \
or self.utilities.isPasswordText(obj):
return default.Script.getTextLineAtCaret(self, obj, offset)
# Find the current line.
#
contextObj, contextOffset = self.getCaretContext()
contextOffset = max(0, contextOffset)
contents = self.currentLineContents
if self.findObjectOnLine(contextObj, contextOffset, contents) < 0:
contents = self.getLineContentsAtOffset(contextObj, contextOffset)
# Determine the caretOffset.
#
if self.utilities.isSameObject(obj, contextObj):
caretOffset = contextOffset
else:
try:
text = obj.queryText()
except:
caretOffset = 0
else:
caretOffset = text.caretOffset
# The reason we typically use this method is to present the contents
# of the current line, so our initial assumption is that the obj
# being passed in is also on this line. We'll try that first. We
# might have multiple instances of obj, in which case we'll have
# to consider the offset as well.
#
for content in contents:
candidate, startOffset, endOffset, string = content
if self.utilities.isSameObject(candidate, obj) \
and (offset is None or (startOffset <= offset <= endOffset)):
return string.encode("UTF-8"), caretOffset, startOffset
# If we're still here, obj presumably is not on this line. This
# shouldn't happen, but if it does we'll let the default script
# handle it for now.
#
#print "getTextLineAtCaret failed"
return default.Script.getTextLineAtCaret(self, obj, offset)
def searchForCaretLocation(self, acc):
"""Attempts to locate the caret on the page independent of our
caret context. This functionality is needed when a page loads
and the URL is for a fragment (anchor, id, named object) within
that page.
Arguments:
- acc: The top-level accessible in which we suspect to find the
caret (most likely the document frame).
Returns the [obj, caretOffset] containing the caret if it can
be determined. Otherwise [None, -1] is returned.
"""
context = [None, -1]
while acc:
try:
offset = acc.queryText().caretOffset
except:
acc = None
else:
context = [acc, offset]
childIndex = self.getChildIndex(acc, offset)
if childIndex >= 0 and acc.childCount:
acc = acc[childIndex]
else:
break
return context
def getCaretContext(self, includeNonText=True):
"""Returns the current [obj, caretOffset] if defined. If not,
it returns the first [obj, caretOffset] found by an in order
traversal from the beginning of the document."""
# We keep a context for each page tab shown.
# [[[TODO: WDW - probably should figure out how to destroy
# these contexts when a tab is killed.]]]
#
documentFrame = self.utilities.documentFrame()
if not documentFrame:
return [None, -1]
try:
return self._documentFrameCaretContext[hash(documentFrame)]
except:
# If we don't have a context, we should attempt to see if we
# can find the caret first. Failing that, we'll start at the
# top.
#
[obj, caretOffset] = self.searchForCaretLocation(documentFrame)
self._documentFrameCaretContext[hash(documentFrame)] = \
self.findNextCaretInOrder(obj,
max(-1, caretOffset - 1),
includeNonText)
[obj, caretOffset] = \
self._documentFrameCaretContext[hash(documentFrame)]
# Yelp is seemingly fond of killing children for sport. Better
# check for that.
#
try:
state = obj.getState()
except:
return [None, -1]
else:
if state.contains(pyatspi.STATE_DEFUNCT):
#print "getCaretContext: defunct object", obj
debug.printStack(debug.LEVEL_WARNING)
[obj, caretOffset] = [None, -1]
return [obj, caretOffset]
def getCharacterAtOffset(self, obj, characterOffset):
"""Returns the character at the given characterOffset in the
given object or None if the object does not implement the
accessible text specialization.
"""
try:
unicodeText = self.utilities.unicodeText(obj)
return unicodeText[characterOffset].encode("UTF-8")
except:
return None
def getWordContentsAtOffset(self, obj, characterOffset, boundary=None):
"""Returns an ordered list where each element is composed of an
[obj, startOffset, endOffset, string] tuple. The list is created
via an in-order traversal of the document contents starting at
the given object and characterOffset. The first element in
the list represents the beginning of the word. The last
element in the list represents the character just before the
beginning of the next word.
Arguments:
-obj: the object to start at
-characterOffset: the characterOffset in the object
-boundary: the pyatsi word boundary to use
"""
if not obj:
return []
boundary = boundary or pyatspi.TEXT_BOUNDARY_WORD_START
text = self.utilities.queryNonEmptyText(obj)
if text:
word = text.getTextAtOffset(characterOffset, boundary)
if word[1] < characterOffset <= word[2]:
characterOffset = word[1]
contents = self.getObjectsFromEOCs(obj, characterOffset, boundary)
if len(contents) > 1 \
and contents[0][0].getRole() == pyatspi.ROLE_LIST_ITEM:
contents = [contents[0]]
return contents
def getLineContentsAtOffset(self, obj, offset):
"""Returns an ordered list where each element is composed of an
[obj, startOffset, endOffset, string] tuple. The list is created
via an in-order traversal of the document contents starting at
the given object and characterOffset. The first element in
the list represents the beginning of the line. The last
element in the list represents the character just before the
beginning of the next line.
Arguments:
-obj: the object to start at
-offset: the character offset in the object
"""
if not obj:
return []
# If it's an ARIA widget, we want the default generators to give
# us all the details.
#
if not self.isNavigableAria(obj):
if not self.isAriaWidget(obj):
obj = obj.parent
objects = [[obj, 0, 1, ""]]
ext = obj.queryComponent().getExtents(0)
extents = [ext.x, ext.y, ext.width, ext.height]
for i in range(obj.getIndexInParent() + 1, obj.parent.childCount):
newObj = obj.parent[i]
ext = newObj.queryComponent().getExtents(0)
newExtents = [ext.x, ext.y, ext.width, ext.height]
if self.onSameLine(extents, newExtents):
objects.append([newObj, 0, 1, ""])
else:
break
for i in range(obj.getIndexInParent() - 1, -1, -1):
newObj = obj.parent[i]
ext = newObj.queryComponent().getExtents(0)
newExtents = [ext.x, ext.y, ext.width, ext.height]
if self.onSameLine(extents, newExtents):
objects[0:0] = [[newObj, 0, 1, ""]]
else:
break
return objects
boundary = pyatspi.TEXT_BOUNDARY_LINE_START
# Find the beginning of this line w.r.t. this object.
#
text = self.utilities.queryNonEmptyText(obj)
if not text:
offset = 0
else:
[line, start, end] = text.getTextAtOffset(offset, boundary)
# Unfortunately, we sometimes get bogus results from Gecko when
# we ask for this line. If the offset is not within the range of
# characters on this line, try the character reported as the end.
#
if not (start <= offset < end):
[line, start, end] = text.getTextAfterOffset(end, boundary)
# If we're still seeing bogusity, which we only seem to see when
# moving up, locate the previous character and use it instead.
#
if not (start <= offset < end):
pObj, pOffset = self.findPreviousCaretInOrder(obj, offset)
if pObj:
obj, offset = pObj, pOffset
text = self.utilities.queryNonEmptyText(obj)
if text:
[line, start, end] = \
text.getTextAtOffset(offset, boundary)
if start <= offset < end:
# So far so good. If the line doesn't begin with an EOC, we
# have our first character for this object.
#
if not line.startswith(self.EMBEDDED_OBJECT_CHARACTER):
offset = start
else:
# The line may begin with a link, or it may begin with
# an anchor which makes this text something one can jump
# to via a link. Anchors are bad.
#
childIndex = self.getChildIndex(obj, start)
if childIndex >= 0:
child = obj[childIndex]
childText = self.utilities.queryNonEmptyText(child)
if not childText:
# It's probably an anchor. It might be something
# else, but that's okay because we do another
# check later to make sure we have everything on
# the left. Set the offset to just after the
# assumed anchor.
#
offset = start + 1
elif obj.getRole() == pyatspi.ROLE_PARAGRAPH \
and child.getRole() == pyatspi.ROLE_PARAGRAPH:
# We don't normally see nested paragraphs. But
# they occur at least when a paragraph begins
# with a multi-line-high character. If we set
# the beginning of this line to that initial
# character, we'll get stuck. See bug 592383.
#
if end - start > 1 and end - offset == 1:
# We must be Up Arrowing. Set the offset to
# just past the EOC so that we present the
# line rather than saying "blank."
#
offset = start + 1
else:
# It's a link that ends on our left. Who knows
# where it starts? Might be on the previous
# line. We will assume that it begins on this
# line if the start offset is 0. However, it
# might be an image link which occupies more
# than just this line. To be safe, we'll also
# look to be sure that the text does not start
# with an embedded object character. See bug
# 587794.
#
cOffset = childText.characterCount - 1
[cLine, cStart, cEnd] = \
childText.getTextAtOffset(cOffset, boundary)
if cStart == 0 \
and not cLine.startswith(\
self.EMBEDDED_OBJECT_CHARACTER) \
and obj.getRole() != pyatspi.ROLE_PANEL:
# It starts on this line.
#
obj = child
offset = cStart
else:
offset = start + 1
extents = self.getExtents(obj, offset, offset + 1)
# Get the objects on this line.
#
objects = self.getObjectsFromEOCs(obj, offset, boundary)
# Check for things on the left.
#
lastExtents = (0, 0, 0, 0)
done = False
while not done:
[firstObj, start, end, string] = objects[0]
[prevObj, pOffset] = self.findPreviousCaretInOrder(firstObj, start)
if not prevObj or self.utilities.isSameObject(prevObj, firstObj):
break
text = self.utilities.queryNonEmptyText(prevObj)
if text:
line = text.getTextAtOffset(pOffset, boundary)
pOffset = line[1]
# If a line begins with a link, getTextAtOffset might
# return a zero-length string. If we have a valid offset
# increment the pOffset by 1 before getting the extents.
#
if line[1] > 0 and line[1] == line[2]:
pOffset += 1
prevExtents = self.getExtents(prevObj, pOffset, pOffset + 1)
if self.onSameLine(extents, prevExtents) \
and extents != prevExtents \
and lastExtents != prevExtents:
toAdd = self.getObjectsFromEOCs(prevObj, pOffset, boundary)
# Depending on the line, there's a chance that we got our
# current object as part of toAdd. Check for dupes and just
# add up to the current object if we find them.
#
try:
index = toAdd.index(objects[0])
except:
index = len(toAdd)
objects[0:0] = toAdd[0:index]
else:
break
lastExtents = prevExtents
# Check for things on the right.
#
lastExtents = (0, 0, 0, 0)
done = False
while not done:
[lastObj, start, end, string] = objects[-1]
# The offset reported as the end offset can vary with Gecko.
# If the offset is one bigger than we expect, we are in danger
# of skipping over an object. Therefore, start by decrementing
# the end offset by 1. If we find the same object, try again.
#
[nextObj, nOffset] = self.findNextCaretInOrder(lastObj, end - 1)
if self.utilities.isSameObject(lastObj, nextObj):
[nextObj, nOffset] = \
self.findNextCaretInOrder(nextObj, nOffset)
if not nextObj or self.utilities.isSameObject(nextObj, lastObj):
break
text = self.utilities.queryNonEmptyText(nextObj)
if text:
line = text.getTextAfterOffset(nOffset, boundary)
nOffset = line[1]
nextExtents = self.getExtents(nextObj, nOffset, nOffset + 1)
if self.onSameLine(extents, nextExtents) \
and extents != nextExtents \
and lastExtents != nextExtents \
or nextExtents == (0, 0, 0, 0):
toAdd = self.getObjectsFromEOCs(nextObj, nOffset, boundary)
objects.extend(toAdd)
elif (nextObj.getRole() in [pyatspi.ROLE_SECTION,
pyatspi.ROLE_TABLE_CELL] \
and self.isUselessObject(nextObj)):
toAdd = self.getObjectsFromEOCs(nextObj, nOffset, boundary)
done = True
for item in toAdd:
itemExtents = self.getExtents(item[0], item[1], item[2])
if self.onSameLine(extents, itemExtents):
objects.append(item)
done = False
if done:
break
else:
break
lastExtents = nextExtents
return objects
def getObjectContentsAtOffset(self, obj, characterOffset):
"""Returns an ordered list where each element is composed of
an [obj, startOffset, endOffset, string] tuple. The list is
created via an in-order traversal of the document contents
starting and stopping at the given object.
"""
return self.getObjectsFromEOCs(obj, characterOffset)
####################################################################
# #
# Methods to speak current objects. #
# #
####################################################################
# [[[TODO: WDW - this needs to be moved to the speech generator.]]]
#
def getACSS(self, obj, string):
"""Returns the ACSS to speak anything for the given obj."""
if obj.getRole() == pyatspi.ROLE_LINK:
acss = self.voices[settings.HYPERLINK_VOICE]
elif string and isinstance(string, basestring) \
and string.decode("UTF-8").isupper() \
and string.decode("UTF-8").strip().isalpha():
acss = self.voices[settings.UPPERCASE_VOICE]
else:
acss = self.voices[settings.DEFAULT_VOICE]
return acss
def getUtterancesFromContents(self, contents, speakRole=True):
"""Returns a list of [text, acss] tuples based upon the list
of [obj, startOffset, endOffset, string] tuples passed in.
Arguments:
-contents: a list of [obj, startOffset, endOffset, string] tuples
-speakRole: if True, speak the roles of objects
"""
if not len(contents):
return []
# Even if we want to speakRole, we don't want to do that for the
# document frame. And we're going to special-case headings so that
# that we don't overspeak heading role info, which we're in danger
# of doing if a heading includes links or images.
#
doNotSpeakRoles = [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_HEADING]
utterances = []
prevObj = None
for content in contents:
[obj, startOffset, endOffset, string] = content
role = obj.getRole()
# If we don't have an object, there's nothing to do. If we have
# a string, but it consists solely of spaces, we have nothing to
# say. If it's a label for an object in our contents, we'll get
# that label via the speech generator for the object.
#
if not obj \
or len(string) and not len(string.strip(" ")) \
or self.isLabellingContents(obj, contents):
continue
# Thunderbird now does something goofy with smileys in
# email: exposes them as a nested paragraph with a name
# consisting of the punctuation used to create the smiley
# and an empty accessible text object. This causes us to
# speak tutorial info for each smiley. :-( type in text.
#
elif role == pyatspi.ROLE_PARAGRAPH and not len(string):
string = obj.name
# We also see goofiness in some pages. That can cause
# SayAll by Sentence to spit up. See bug 591351. So
# if we still do not have string and if we've got
# more than object in contents, let's dump this one.
#
if len(contents) > 1 and not len(string):
continue
# If it is a "useless" image (i.e. not a link, no associated
# text), ignore it, unless it's the only thing here.
#
elif role == pyatspi.ROLE_IMAGE and self.isUselessObject(obj) \
and len(contents) > 1:
continue
# If the focused item is a checkbox or a radio button for which
# we had to guess the label, odds are that the guessed label is
# immediately to the right. Under these circumstances, we'll
# double speak the "label". It would be nice to avoid that.
# [[[TODO - JD: This is the simple version. It does not handle
# the possibility of the fake label being comprised of multiple
# objects.]]]
#
if prevObj \
and prevObj.getRole() in [pyatspi.ROLE_CHECK_BOX,
pyatspi.ROLE_RADIO_BUTTON] \
and prevObj.getState().contains(pyatspi.STATE_FOCUSED):
if self.guessTheLabel(prevObj) == string.strip():
continue
# The radio button's label gets added to the context in
# default.locusOfFocusChanged() and not through the speech
# generator -- unless we wind up having to guess the label.
# Therefore, if we have a valid label for a radio button,
# we need to add it here.
#
if (role == pyatspi.ROLE_RADIO_BUTTON) \
and not self.isAriaWidget(obj):
label = self.utilities.displayedLabel(obj)
if label:
utterances.append([label, self.getACSS(obj, label)])
# If we don't have a string, then use the speech generator.
# Otherwise, we'll want to speak the string and possibly the
# role.
#
if not len(string) \
or self.utilities.isEntry(obj) \
or self.utilities.isPasswordText(obj):
rv = self.speechGenerator.generateSpeech(obj)
# Crazy crap to make clump and friends happy until we can
# kill them. (They don't deal well with what the speech
# generator provides.)
for item in rv:
if isinstance(item, basestring):
utterances.append([item, self.getACSS(obj, item)])
else:
utterances.append([string, self.getACSS(obj, string)])
if speakRole and not role in doNotSpeakRoles:
utterance = self.speechGenerator.getRoleName(obj)
if utterance:
utterances.append(utterance)
# If the object is a heading, or is contained within a heading,
# speak that role information at the end of the object.
#
isLastObject = (contents.index(content) == (len(contents) - 1))
isHeading = (role == pyatspi.ROLE_HEADING)
if speakRole and (isLastObject or isHeading):
if isHeading:
heading = obj
else:
heading = self.utilities.ancestorWithRole(
obj,
[pyatspi.ROLE_HEADING],
[pyatspi.ROLE_DOCUMENT_FRAME])
if heading:
utterance = self.speechGenerator.getRoleName(heading)
if utterance:
utterances.append(utterance)
prevObj = obj
return utterances
def clumpUtterances(self, utterances):
"""Returns a list of utterances clumped together by acss.
Arguments:
-utterances: unclumped utterances
-speakRole: if True, speak the roles of objects
"""
clumped = []
for [element, acss] in utterances:
if len(clumped) == 0:
clumped = [[element, acss]]
elif acss == clumped[-1][1] \
and isinstance(element, basestring) \
and isinstance(clumped[-1][0], basestring):
clumped [-1][0] = clumped[-1][0].rstrip(" ")
clumped[-1][0] += " " + element
else:
clumped.append([element, acss])
if (len(clumped) == 1) and (clumped[0][0] == "\n"):
if _settingsManager.getSetting('speakBlankLines'):
# Translators: "blank" is a short word to mean the
# user has navigated to an empty line.
#
return [[_("blank"), self.voices[settings.SYSTEM_VOICE]]]
if len(clumped) and isinstance(clumped[-1][0], basestring):
clumped[-1][0] = clumped[-1][0].rstrip(" ")
return clumped
def speakContents(self, contents, speakRole=True):
"""Speaks each string in contents using the associated voice/acss"""
utterances = self.getUtterancesFromContents(contents, speakRole)
clumped = self.clumpUtterances(utterances)
for [element, acss] in clumped:
if isinstance(element, basestring):
element = self.utilities.adjustForRepeats(element)
speech.speak(element, acss, False)
def speakCharacterAtOffset(self, obj, characterOffset):
"""Speaks the character at the given characterOffset in the
given object."""
character = self.getCharacterAtOffset(obj, characterOffset)
self.speakMisspelledIndicator(obj, characterOffset)
if obj:
if character and character != self.EMBEDDED_OBJECT_CHARACTER:
speech.speakCharacter(character,
self.getACSS(obj, character))
elif not self.utilities.isEntry(obj):
# 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)
####################################################################
# #
# Methods to navigate to previous and next objects. #
# #
####################################################################
def setCaretPosition(self, obj, characterOffset):
"""Sets the caret position to the given character offset in the
given object.
"""
# Clear the flat review context if the user is currently in a
# flat review.
#
if self.flatReviewContext:
self.toggleFlatReviewMode()
caretContext = self.getCaretContext()
# Save where we are in this particular document frame.
# We do this because the user might have several URLs
# open in several different tabs, and we keep track of
# where the caret is for each documentFrame.
#
documentFrame = self.utilities.documentFrame()
if documentFrame:
self._documentFrameCaretContext[hash(documentFrame)] = caretContext
if caretContext == [obj, characterOffset]:
return
self.setCaretContext(obj, characterOffset)
# If the item is a focusable list in an HTML form, we're here
# because we've arrowed to it. We don't want to grab focus on
# it and trap the user in the list. The same is true for combo
# boxes.
#
if obj \
and obj.getRole() in [pyatspi.ROLE_LIST, pyatspi.ROLE_COMBO_BOX] \
and obj.getState().contains(pyatspi.STATE_FOCUSABLE):
characterOffset = self.utilities.characterOffsetInParent(obj)
obj = obj.parent
self.setCaretContext(obj, characterOffset)
# Reset focus if need be.
#
if obj != orca_state.locusOfFocus:
orca.setLocusOfFocus(None, obj, notifyScript=False)
# We'd like the object to have focus if it can take focus.
# Otherwise, we bubble up until we find a parent that can
# take focus. This is to allow us to help force focus out
# of something such as a text area and back into the
# document content.
#
if script_settings.grabFocusOnAncestor:
self._objectForFocusGrab = obj
else:
self._objectForFocusGrab = None
while self._objectForFocusGrab and obj:
role = self._objectForFocusGrab.getRole()
# If we're within a link whose children contain the text,
# grabbing focus on the link will result in our looping
# back to the link and never being able to arrow through
# the text.
#
if role == pyatspi.ROLE_LINK \
and self.utilities.queryNonEmptyText(obj):
self._objectForFocusGrab = None
break
if self._objectForFocusGrab.getState().contains(\
pyatspi.STATE_FOCUSABLE):
break
# Links in image maps seem to lack state focusable. If we're
# on such an object, we still want to grab focus on it.
#
elif role == pyatspi.ROLE_LINK:
parent = self._objectForFocusGrab.parent
if parent.getRole() == pyatspi.ROLE_IMAGE:
break
self._objectForFocusGrab = self._objectForFocusGrab.parent
# [[[JD - I *think* we still want to do a focus grab, even with
# the issues identified in bug 608149. Nothing bad should result
# from grabbing focus on a non-focusable object. But I might be
# wrong.]]]
#
if obj and not self._objectForFocusGrab:
obj.queryComponent().grabFocus()
if self._objectForFocusGrab:
# [[[See https://bugzilla.mozilla.org/show_bug.cgi?id=363214.
# We need to set focus on the parent of the document frame.]]]
#
# [[[WDW - additional note - just setting focus on the
# first focusable object seems to do the trick, so we
# won't follow the advice from 363214. Besides, if we
# follow that advice, it doesn't work.]]]
#
#if objectForFocus.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
# objectForFocus = objectForFocus.parent
self._objectForFocusGrab.queryComponent().grabFocus()
text = self.utilities.queryNonEmptyText(obj)
if text:
text.setCaretOffset(characterOffset)
if characterOffset == text.characterCount:
characterOffset -= 1
mag.magnifyAccessible(None,
obj,
self.getExtents(obj,
characterOffset,
characterOffset + 1))
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:
# Translators: hovering the mouse over certain objects on a
# web page causes a new object to appear such as a pop-up
# menu. Orca has a command will move the user to the object
# which just appeared as a result of the user hovering the
# mouse. If this command fails, Orca will present this message.
#
self.presentMessage(_("Mouse over object 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.findFirstCaretContext(obj, offset)
if obj and obj.getState().contains(pyatspi.STATE_FOCUSABLE):
obj.queryComponent().grabFocus()
elif obj:
contents = self.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.findNextCaretInOrder(obj, offset)
contents = self.getObjectContentsAtOffset(obj, offset)
self.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.findFirstCaretContext(obj, offset)
if obj and obj.getState().contains(pyatspi.STATE_FOCUSABLE):
obj.queryComponent().grabFocus()
elif obj:
self.setCaretPosition(obj, offset)
self.speakContents(self.getObjectContentsAtOffset(obj, offset))
self.updateBraille(obj)
self.inMouseOverObject = False
self.lastMouseOverObject = None
def goNextCharacter(self, inputEvent):
"""Positions the caret offset to the next character or object
in the document window.
"""
[obj, characterOffset] = self.getCaretContext()
while obj:
[obj, characterOffset] = self.findNextCaretInOrder(obj,
characterOffset)
if obj and obj.getState().contains(pyatspi.STATE_VISIBLE):
break
if not obj:
[obj, characterOffset] = self.getBottomOfFile()
else:
self.speakCharacterAtOffset(obj, characterOffset)
self.setCaretPosition(obj, characterOffset)
self.updateBraille(obj)
def goPreviousCharacter(self, inputEvent):
"""Positions the caret offset to the previous character or object
in the document window.
"""
[obj, characterOffset] = self.getCaretContext()
while obj:
[obj, characterOffset] = self.findPreviousCaretInOrder(
obj, characterOffset)
if obj and obj.getState().contains(pyatspi.STATE_VISIBLE):
break
if not obj:
[obj, characterOffset] = self.getTopOfFile()
else:
self.speakCharacterAtOffset(obj, characterOffset)
self.setCaretPosition(obj, characterOffset)
self.updateBraille(obj)
def goPreviousWord(self, inputEvent):
"""Positions the caret offset to beginning of the previous
word or object in the document window.
"""
[obj, characterOffset] = self.getCaretContext()
# Make sure we have a word.
#
[obj, characterOffset] = \
self.findPreviousCaretInOrder(obj, characterOffset)
# To be consistent with Gecko's native navigation, we want to move
# to the next (or technically the previous) word start boundary.
#
boundary = pyatspi.TEXT_BOUNDARY_WORD_START
contents = self.getWordContentsAtOffset(obj, characterOffset, boundary)
if not len(contents):
return
[obj, startOffset, endOffset, string] = contents[0]
if len(contents) == 1 \
and endOffset - startOffset == 1 \
and self.getCharacterAtOffset(obj, startOffset) == " ":
# Our "word" is just a space. This can happen if the previous
# word was a mark of punctuation surrounded by whitespace (e.g.
# " | ").
#
[obj, characterOffset] = \
self.findPreviousCaretInOrder(obj, startOffset)
contents = \
self.getWordContentsAtOffset(obj, characterOffset, boundary)
if len(contents):
[obj, startOffset, endOffset, string] = contents[0]
self.setCaretPosition(obj, startOffset)
self.updateBraille(obj)
self.speakMisspelledIndicator(obj, startOffset)
self.speakContents(contents)
def goNextWord(self, inputEvent):
"""Positions the caret offset to the end of next word or object
in the document window.
"""
[obj, characterOffset] = self.getCaretContext()
# Make sure we have a word.
#
characterOffset = max(0, characterOffset - 1)
[obj, characterOffset] = \
self.findNextCaretInOrder(obj, characterOffset)
# To be consistent with Gecko's native navigation, we want to
# move to the next word end boundary.
#
boundary = pyatspi.TEXT_BOUNDARY_WORD_END
contents = self.getWordContentsAtOffset(obj, characterOffset, boundary)
if not (len(contents) and contents[-1][2]):
return
[obj, startOffset, endOffset, string] = contents[-1]
self.setCaretPosition(obj, endOffset)
self.updateBraille(obj)
# Because we're getting the word based on the WORD_END boundary
# rather than the WORD_START boundary, we need to increment our
# offset.
#
self.speakMisspelledIndicator(obj, startOffset + 1)
self.speakContents(contents)
def findPreviousLine(self, obj, characterOffset, updateCache=True):
"""Locates the caret offset at the previous line in the document
window.
Arguments:
-obj: the object from which the search should begin
-characterOffset: the offset within obj from which the search should
begin
-updateCache: whether or not we should update the line cache
Returns the [obj, characterOffset] at the beginning of the line.
"""
if not obj:
[obj, characterOffset] = self.getCaretContext()
if not obj:
return self.getTopOfFile()
currentLine = self.currentLineContents
index = self.findObjectOnLine(obj, characterOffset, currentLine)
if index < 0:
text = self.utilities.queryNonEmptyText(obj)
if text and text.characterCount == characterOffset:
characterOffset -= 1
currentLine = self.getLineContentsAtOffset(obj, characterOffset)
prevObj = currentLine[0][0]
prevOffset = currentLine[0][1]
[prevObj, prevOffset] = \
self.findPreviousCaretInOrder(currentLine[0][0], currentLine[0][1])
extents = self.getExtents(currentLine[0][0],
currentLine[0][1],
currentLine[0][2])
prevExtents = self.getExtents(prevObj, prevOffset, prevOffset + 1)
while self.onSameLine(extents, prevExtents) \
and (extents != prevExtents):
[prevObj, prevOffset] = \
self.findPreviousCaretInOrder(prevObj, prevOffset)
prevExtents = self.getExtents(prevObj, prevOffset, prevOffset + 1)
# If the user did some back-to-back arrowing, we might already have
# the line contents.
#
prevLine = self._previousLineContents
index = self.findObjectOnLine(prevObj, prevOffset, prevLine)
if index < 0:
prevLine = self.getLineContentsAtOffset(prevObj, prevOffset)
if not prevLine:
return [None, -1]
prevObj = prevLine[0][0]
prevOffset = prevLine[0][1]
failureCount = 0
while failureCount < 5 and prevObj and currentLine == prevLine:
# For some reason we're stuck. We'll try a few times by
# caret before trying by object.
#
# print "find prev line failed", prevObj, prevOffset
[prevObj, prevOffset] = \
self.findPreviousCaretInOrder(prevObj, prevOffset)
prevLine = self.getLineContentsAtOffset(prevObj, prevOffset)
failureCount += 1
if currentLine == prevLine:
# print "find prev line still stuck", prevObj, prevOffset
documentFrame = self.utilities.documentFrame()
prevObj = self.findPreviousObject(prevObj, documentFrame)
prevOffset = 0
[prevObj, prevOffset] = self.findNextCaretInOrder(prevObj,
prevOffset - 1)
if not script_settings.arrowToLineBeginning:
extents = self.getExtents(obj,
characterOffset,
characterOffset + 1)
oldX = extents[0]
for item in prevLine:
extents = self.getExtents(item[0], item[1], item[1] + 1)
newX1 = extents[0]
newX2 = newX1 + extents[2]
if newX1 < oldX <= newX2:
newObj = item[0]
newOffset = 0
text = self.utilities.queryNonEmptyText(prevObj)
if text:
newY = extents[1] + extents[3] / 2
newOffset = text.getOffsetAtPoint(oldX, newY, 0)
if 0 <= newOffset <= characterOffset:
prevOffset = newOffset
prevObj = newObj
break
if updateCache:
self._nextLineContents = self.currentLineContents
self.currentLineContents = prevLine
return [prevObj, prevOffset]
def findNextLine(self, obj, characterOffset, updateCache=True):
"""Locates the caret offset at the next line in the document
window.
Arguments:
-obj: the object from which the search should begin
-characterOffset: the offset within obj from which the search should
begin
-updateCache: whether or not we should update the line cache
Returns the [obj, characterOffset] at the beginning of the line.
"""
if not obj:
[obj, characterOffset] = self.getCaretContext()
if not obj:
return self.getBottomOfFile()
currentLine = self.currentLineContents
index = self.findObjectOnLine(obj, characterOffset, currentLine)
if index < 0:
currentLine = self.getLineContentsAtOffset(obj, characterOffset)
[nextObj, nextOffset] = \
self.findNextCaretInOrder(currentLine[-1][0],
currentLine[-1][2] - 1)
extents = self.getExtents(currentLine[-1][0],
currentLine[-1][1],
currentLine[-1][2])
nextExtents = self.getExtents(nextObj, nextOffset, nextOffset + 1)
while self.onSameLine(extents, nextExtents) \
and (extents != nextExtents):
[nextObj, nextOffset] = \
self.findNextCaretInOrder(nextObj, nextOffset)
nextExtents = self.getExtents(nextObj, nextOffset, nextOffset + 1)
# If the user did some back-to-back arrowing, we might already have
# the line contents.
#
nextLine = self._nextLineContents
index = self.findObjectOnLine(nextObj, nextOffset, nextLine)
if index < 0:
nextLine = self.getLineContentsAtOffset(nextObj, nextOffset)
if not nextLine:
return [None, -1]
failureCount = 0
while failureCount < 5 and nextObj and currentLine == nextLine:
# For some reason we're stuck. We'll try a few times by
# caret before trying by object.
#
#print "find next line failed", nextObj, nextOffset
[nextObj, nextOffset] = \
self.findNextCaretInOrder(nextObj, nextOffset)
if nextObj:
nextLine = self.getLineContentsAtOffset(nextObj, nextOffset)
failureCount += 1
if currentLine == nextLine:
#print "find next line still stuck", nextObj, nextOffset
documentFrame = self.utilities.documentFrame()
nextObj = self.findNextObject(nextObj, documentFrame)
nextOffset = 0
# On a page which contains tables which are not only nested, but
# are surrounded by line break characters and/or embedded within
# a paragraph or span, there's an excellent chance that we'll skip
# right over the nested content. See bug #555055. If we can detect
# this condition, we should set the nextOffset to the EOC which
# represents the nested content before findNextCaretInOrder does
# its thing.
#
if nextOffset == 0 \
and self.getCharacterAtOffset(nextObj, nextOffset) == "\n" \
and self.getCharacterAtOffset(nextObj, nextOffset + 1) == \
self.EMBEDDED_OBJECT_CHARACTER:
nextOffset += 1
[nextObj, nextOffset] = \
self.findNextCaretInOrder(nextObj, max(0, nextOffset) - 1)
if not script_settings.arrowToLineBeginning:
extents = self.getExtents(obj,
characterOffset,
characterOffset + 1)
oldX = extents[0]
for item in nextLine:
extents = self.getExtents(item[0], item[1], item[1] + 1)
newX1 = extents[0]
newX2 = newX1 + extents[2]
if newX1 < oldX <= newX2:
newObj = item[0]
newOffset = 0
text = self.utilities.queryNonEmptyText(nextObj)
if text:
newY = extents[1] + extents[3] / 2
newOffset = text.getOffsetAtPoint(oldX, newY, 0)
if newOffset >= 0:
nextOffset = newOffset
nextObj = newObj
break
if updateCache:
self._previousLineContents = self.currentLineContents
self.currentLineContents = nextLine
return [nextObj, nextOffset]
def goPreviousLine(self, inputEvent):
"""Positions the caret offset at the previous line in the document
window, attempting to preserve horizontal caret position.
Returns True if we actually moved.
"""
[obj, characterOffset] = self.getCaretContext()
[previousObj, previousCharOffset] = \
self.findPreviousLine(obj, characterOffset)
if not previousObj:
return False
self.setCaretPosition(previousObj, previousCharOffset)
self.presentLine(previousObj, previousCharOffset)
# Debug...
#
#contents = self.getLineContentsAtOffset(previousObj,
# previousCharOffset)
#self.dumpContents(inputEvent, contents)
return True
def goNextLine(self, inputEvent):
"""Positions the caret offset at the next line in the document
window, attempting to preserve horizontal caret position.
Returns True if we actually moved.
"""
[obj, characterOffset] = self.getCaretContext()
[nextObj, nextCharOffset] = self.findNextLine(obj, characterOffset)
if not nextObj:
return False
self.setCaretPosition(nextObj, nextCharOffset)
self.presentLine(nextObj, nextCharOffset)
# Debug...
#
#contents = self.getLineContentsAtOffset(nextObj, nextCharOffset)
#self.dumpContents(inputEvent, contents)
return True
def goBeginningOfLine(self, inputEvent):
"""Positions the caret offset at the beginning of the line."""
[obj, characterOffset] = self.getCaretContext()
line = self.getLineContentsAtOffset(obj, characterOffset)
obj, characterOffset = line[0][0], line[0][1]
self.setCaretPosition(obj, characterOffset)
if not isinstance(orca_state.lastInputEvent, input_event.BrailleEvent):
self.speakCharacterAtOffset(obj, characterOffset)
self.updateBraille(obj)
def goEndOfLine(self, inputEvent):
"""Positions the caret offset at the end of the line."""
[obj, characterOffset] = self.getCaretContext()
line = self.getLineContentsAtOffset(obj, characterOffset)
obj, characterOffset = line[-1][0], line[-1][2] - 1
self.setCaretPosition(obj, characterOffset)
if not isinstance(orca_state.lastInputEvent, input_event.BrailleEvent):
self.speakCharacterAtOffset(obj, characterOffset)
self.updateBraille(obj)
def goTopOfFile(self, inputEvent):
"""Positions the caret offset at the beginning of the document."""
[obj, characterOffset] = self.getTopOfFile()
self.setCaretPosition(obj, characterOffset)
self.presentLine(obj, characterOffset)
def goBottomOfFile(self, inputEvent):
"""Positions the caret offset at the end of the document."""
[obj, characterOffset] = self.getBottomOfFile()
self.setCaretPosition(obj, characterOffset)
self.presentLine(obj, characterOffset)
def expandComboBox(self, inputEvent):
"""If focus is on a menu item, but the containing combo box does not
have focus, give the combo box focus and expand it. Note that this
is necessary because with Orca controlling the caret it is possible
to arrow to a menu item within the combo box without actually giving
the containing combo box focus.
"""
[obj, characterOffset] = self.getCaretContext()
comboBox = None
if obj.getRole() == pyatspi.ROLE_MENU_ITEM:
comboBox = self.utilities.ancestorWithRole(
obj, [pyatspi.ROLE_COMBO_BOX], [pyatspi.ROLE_DOCUMENT_FRAME])
else:
index = self.getChildIndex(obj, characterOffset)
if index >= 0:
comboBox = obj[index]
if not comboBox:
return
try:
action = comboBox.queryAction()
except:
pass
else:
orca.setLocusOfFocus(None, comboBox)
comboBox.queryComponent().grabFocus()
for i in range(0, action.nActions):
name = action.getName(i)
# Translators: this is the action name for the 'open' action.
#
if name in ["open", _("open")]:
action.doAction(i)
break
def goPreviousObjectInOrder(self, inputEvent):
"""Go to the previous object in order, regardless of type or size."""
[obj, characterOffset] = self.getCaretContext()
# Work our way out of form lists and combo boxes.
#
if obj and obj.getState().contains(pyatspi.STATE_SELECTABLE):
obj = obj.parent.parent
characterOffset = self.utilities.characterOffsetInParent(obj)
self.currentLineContents = None
characterOffset = max(0, characterOffset)
[prevObj, prevOffset] = [obj, characterOffset]
found = False
mayHaveGoneTooFar = False
line = self.currentLineContents \
or self.getLineContentsAtOffset(obj, characterOffset)
startingPoint = line
useful = self.getMeaningfulObjectsFromLine(line)
while line and not found:
index = self.findObjectOnLine(prevObj, prevOffset, useful)
if not self.utilities.isSameObject(obj, prevObj):
# The question is, have we found the beginning of this
# object? If the offset is 0 or there's more than one
# object on this line and we started on a later line,
# it's safe to assume we've found the beginning.
#
found = (prevOffset == 0) \
or (len(useful) > 1 and line != startingPoint)
# Otherwise, we won't know for certain until we've gone
# to the line(s) before this one and found a different
# object, at which point we may have gone too far.
#
if not found:
mayHaveGoneTooFar = True
obj = prevObj
characterOffset = prevOffset
elif 0 < index < len(useful):
prevObj = useful[index - 1][0]
prevOffset = useful[index - 1][1]
found = (prevOffset == 0) or (index > 1)
if not found:
mayHaveGoneTooFar = True
elif self.utilities.isSameObject(obj, prevObj) \
and 0 == prevOffset < characterOffset:
found = True
if not found:
self._nextLineContents = line
prevLine = self.findPreviousLine(line[0][0], line[0][1])
line = self.currentLineContents
useful = self.getMeaningfulObjectsFromLine(line)
prevObj = useful[-1][0]
prevOffset = useful[-1][1]
if self.currentLineContents == self._nextLineContents:
break
if not found:
# Translators: when the user is attempting to locate a
# particular object and the top of the page or list is
# reached without that object being found, we "wrap" to
# the bottom and continue looking upwards. We need to
# inform the user when this is taking place.
#
self.presentMessage(_("Wrapping to bottom."))
[prevObj, prevOffset] = self.getBottomOfFile()
line = self.getLineContentsAtOffset(prevObj, prevOffset)
useful = self.getMeaningfulObjectsFromLine(line)
if useful:
prevObj = useful[-1][0]
prevOffset = useful[-1][1]
found = not (prevObj is None)
elif mayHaveGoneTooFar and self._nextLineContents:
if not self.utilities.isSameObject(obj, prevObj):
prevObj = useful[index][0]
prevOffset = useful[index][1]
if found:
self.currentLineContents = line
self.setCaretPosition(prevObj, prevOffset)
self.updateBraille(prevObj)
objectContents = self.getObjectContentsAtOffset(prevObj,
prevOffset)
objectContents = [objectContents[0]]
self.speakContents(objectContents)
def goNextObjectInOrder(self, inputEvent):
"""Go to the next object in order, regardless of type or size."""
[obj, characterOffset] = self.getCaretContext()
# Work our way out of form lists and combo boxes.
#
if obj and obj.getState().contains(pyatspi.STATE_SELECTABLE):
obj = obj.parent.parent
characterOffset = self.utilities.characterOffsetInParent(obj)
self.currentLineContents = None
characterOffset = max(0, characterOffset)
[nextObj, nextOffset] = [obj, characterOffset]
found = False
line = self.currentLineContents \
or self.getLineContentsAtOffset(obj, characterOffset)
while line and not found:
useful = self.getMeaningfulObjectsFromLine(line)
index = self.findObjectOnLine(nextObj, nextOffset, useful)
if not self.utilities.isSameObject(obj, nextObj):
nextObj = useful[0][0]
nextOffset = useful[0][1]
found = True
elif 0 <= index < len(useful) - 1:
nextObj = useful[index + 1][0]
nextOffset = useful[index + 1][1]
found = True
else:
self._previousLineContents = line
[nextObj, nextOffset] = self.findNextLine(line[-1][0],
line[-1][2])
line = self.currentLineContents
if self.currentLineContents == self._previousLineContents:
break
if not found:
# Translators: when the user is attempting to locate a
# particular object and the bottom of the page or list is
# reached without that object being found, we "wrap" to the
# top and continue looking downwards. We need to inform the
# user when this is taking place.
#
self.presentMessage(_("Wrapping to top."))
[nextObj, nextOffset] = self.getTopOfFile()
line = self.getLineContentsAtOffset(nextObj, nextOffset)
useful = self.getMeaningfulObjectsFromLine(line)
if useful:
nextObj = useful[0][0]
nextOffset = useful[0][1]
found = not (nextObj is None)
if found:
self.currentLineContents = line
self.setCaretPosition(nextObj, nextOffset)
self.updateBraille(nextObj)
objectContents = self.getObjectContentsAtOffset(nextObj,
nextOffset)
objectContents = [objectContents[0]]
self.speakContents(objectContents)
def advanceLivePoliteness(self, inputEvent):
"""Advances live region politeness level."""
if _settingsManager.getSetting('inferLiveRegions'):
self.liveMngr.advancePoliteness(orca_state.locusOfFocus)
else:
# Translators: this announces to the user that live region
# support has been turned off.
#
self.presentMessage(_("Live region support is off"))
def monitorLiveRegions(self, inputEvent):
if not _settingsManager.getSetting('inferLiveRegions'):
_settingsManager.setSetting('inferLiveRegions', True)
# Translators: this announces to the user that live region
# are being monitored.
#
self.presentMessage(_("Live regions monitoring on"))
else:
_settingsManager.setSetting('inferLiveRegions', False)
# Translators: this announces to the user that live region
# are not being monitored.
#
self.liveMngr.flushMessages()
self.presentMessage(_("Live regions monitoring off"))
def setLivePolitenessOff(self, inputEvent):
if _settingsManager.getSetting('inferLiveRegions'):
self.liveMngr.setLivePolitenessOff()
else:
# Translators: this announces to the user that live region
# support has been turned off.
#
self.presentMessage(_("Live region support is off"))
def reviewLiveAnnouncement(self, inputEvent):
if _settingsManager.getSetting('inferLiveRegions'):
self.liveMngr.reviewLiveAnnouncement( \
int(inputEvent.event_string[1:]))
else:
# Translators: this announces to the user that live region
# support has been turned off.
#
self.presentMessage(_("Live region support is off"))
def toggleCaretNavigation(self, inputEvent):
"""Toggles between Firefox native and Orca caret navigation."""
if script_settings.controlCaretNavigation:
for keyBinding in self.__getArrowBindings().keyBindings:
self.keyBindings.removeByHandler(keyBinding.handler)
script_settings.controlCaretNavigation = False
# Translators: Gecko native caret navigation is where
# Firefox itself controls how the arrow keys move the caret
# around HTML content. It's often broken, so Orca needs
# to provide its own support. As such, Orca offers the user
# the ability to switch between the Firefox mode and the
# Orca mode.
#
string = _("Gecko is controlling the caret.")
else:
script_settings.controlCaretNavigation = True
for keyBinding in self.__getArrowBindings().keyBindings:
self.keyBindings.add(keyBinding)
# Translators: Gecko native caret navigation is where
# Firefox itself controls how the arrow keys move the caret
# around HTML content. It's often broken, so Orca needs
# to provide its own support. As such, Orca offers the user
# the ability to switch between the Firefox mode and the
# Orca mode.
#
string = _("Orca is controlling the caret.")
debug.println(debug.LEVEL_CONFIGURATION, string)
self.presentMessage(string)
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.inDocumentContent(acc):
try:
ai = acc.queryAction()
except NotImplementedError:
return True
default.Script.speakWordUnderMouse(self, acc)