# Orca
#
# Copyright 2010 Joanmarie Diggs.
#
# 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.
"""Commonly-required utility methods needed by -- and potentially
customized by -- application and toolkit scripts. They have
been pulled out from the scripts because certain scripts had
gotten way too large as a result of including these methods."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2010 Joanmarie Diggs."
__license__ = "LGPL"
import pyatspi
import orca.debug as debug
import orca.orca_state as orca_state
import orca.script_utilities as script_utilities
#############################################################################
# #
# Utilities #
# #
#############################################################################
class Utilities(script_utilities.Utilities):
def __init__(self, script):
"""Creates an instance of the Utilities class.
Arguments:
- script: the script with which this instance is associated.
"""
script_utilities.Utilities.__init__(self, script)
#########################################################################
# #
# Utilities for finding, identifying, and comparing accessibles #
# #
#########################################################################
def cellIndex(self, obj):
"""Returns the index of the cell which should be used with the
table interface. This is necessary because we cannot count on
the index we need being the same as the index in the parent.
See, for example, tables with captions and tables with rows
that have attributes."""
index = -1
parent = self.ancestorWithRole(obj,
[pyatspi.ROLE_TABLE,
pyatspi.ROLE_TREE_TABLE,
pyatspi.ROLE_TREE],
[pyatspi.ROLE_DOCUMENT_FRAME])
try:
table = parent.queryTable()
except:
pass
else:
attrs = dict([attr.split(':', 1) for attr in obj.getAttributes()])
index = attrs.get('table-cell-index')
if index:
index = int(index)
else:
index = obj.getIndexInParent()
return index
def displayedText(self, obj):
"""Returns the text being displayed for an object.
Arguments:
- obj: the object
Returns the text being displayed for an object or None if there isn't
any text being shown. Overridden in this script because we have lots
of whitespace we need to remove.
"""
displayedText = script_utilities.Utilities.displayedText(self, obj)
if displayedText \
and not (obj.getState().contains(pyatspi.STATE_EDITABLE) \
or obj.getRole() in [pyatspi.ROLE_ENTRY,
pyatspi.ROLE_PASSWORD_TEXT]):
displayedText = displayedText.strip()
# Some ARIA widgets (e.g. the list items in the chat box
# in gmail) implement the accessible text interface but
# only contain whitespace.
#
if not displayedText \
and obj.getState().contains(pyatspi.STATE_FOCUSED):
label = self.displayedLabel(obj)
if not label:
displayedText = obj.name
return displayedText
def displayedLabel(self, obj):
"""If there is an object labelling the given object, return the
text being displayed for the object labelling this object.
Otherwise, return None. Overridden here to handle instances
of bogus labels and form fields where a lack of labels necessitates
our attempt to guess the text that is functioning as a label.
Argument:
- obj: the object in question
Returns the string of the object labelling this object, or None
if there is nothing of interest here.
"""
string = None
labels = self.labelsForObject(obj)
for label in labels:
# Check to see if the official labels are valid.
#
bogus = False
if self._script.inDocumentContent() \
and obj.getRole() in [pyatspi.ROLE_COMBO_BOX,
pyatspi.ROLE_LIST]:
# Bogus case #1:
# surrounding the entire combo box/list which
# makes the entire combo box's/list's contents serve as the
# label. We can identify this case because the child of the
# label is the combo box/list. See bug #428114, #441476.
#
if label.childCount:
bogus = (label[0].getRole() == obj.getRole())
if not bogus:
# Bogus case #2:
# surrounds not just the text serving as the
# label, but whitespace characters as well (e.g. the text
# serving as the label is on its own line within the HTML).
# Because of the Mozilla whitespace bug, these characters
# will become part of the label which will cause the label
# and name to no longer match and Orca to seemingly repeat
# the label. Therefore, strip out surrounding whitespace.
# See bug #441610 and
# https://bugzilla.mozilla.org/show_bug.cgi?id=348901
#
expandedLabel = self.expandEOCs(label)
if expandedLabel:
string = self.appendString(string, expandedLabel.strip())
return string
def documentFrame(self):
"""Returns the document frame that holds the content being shown."""
# [[[TODO: WDW - this is based upon the 12-Oct-2006 implementation
# that uses the EMBEDS relation on the top level frame as a means
# to find the document frame. Future implementations will break
# this.]]]
#
documentFrame = None
for child in self._script.app:
if child.getRole() == pyatspi.ROLE_FRAME:
relationSet = child.getRelationSet()
for relation in relationSet:
if relation.getRelationType() \
== pyatspi.RELATION_EMBEDS:
documentFrame = relation.getTarget(0)
if documentFrame.getState().contains(
pyatspi.STATE_SHOWING):
break
else:
documentFrame = None
# Certain add-ons can interfere with the above approach. But we
# should have a locusOfFocus. If so look up and try to find the
# document frame. See bug 537303.
#
if not documentFrame:
documentFrame = self.ancestorWithRole(
orca_state.locusOfFocus,
[pyatspi.ROLE_DOCUMENT_FRAME],
[pyatspi.ROLE_FRAME])
return documentFrame
def documentFrameURI(self):
"""Returns the URI of the document frame that is active."""
documentFrame = self.documentFrame()
if documentFrame:
# If the document frame belongs to a Thunderbird message which
# has just been deleted, getAttributes() will crash Thunderbird.
#
if not documentFrame.getState().contains(pyatspi.STATE_DEFUNCT):
attrs = documentFrame.queryDocument().getAttributes()
for attr in attrs:
if attr.startswith('DocURL'):
return attr[7:]
return None
def grabFocusBeforeRouting(self, obj, offset):
"""Whether or not we should perform a grabFocus before routing
the cursor via the braille cursor routing keys.
Arguments:
- obj: the accessible object where the cursor should be routed
- offset: the offset to which it should be routed
Returns True if we should do an explicit grabFocus on obj prior
to routing the cursor.
"""
if obj and obj.getRole() == pyatspi.ROLE_COMBO_BOX \
and not self.isSameObject(obj, orca_state.locusOfFocus):
return True
return False
def isEntry(self, obj):
"""Returns True if we should treat this object as an entry."""
if not obj:
return False
if obj.getRole() == pyatspi.ROLE_ENTRY:
return True
if obj.getState().contains(pyatspi.STATE_EDITABLE) \
and obj.getRole() in [pyatspi.ROLE_DOCUMENT_FRAME,
pyatspi.ROLE_PARAGRAPH,
pyatspi.ROLE_TEXT]:
return True
return False
def isLayoutOnly(self, obj):
"""Returns True if the given object is for layout purposes only."""
if self._script.isUselessObject(obj):
debug.println(debug.LEVEL_FINEST,
"Object deemed to be useless: %s" % obj)
return True
else:
return script_utilities.Utilities.isLayoutOnly(self, obj)
def isPasswordText(self, obj):
"""Returns True if we should treat this object as password text."""
return obj and obj.getRole() == pyatspi.ROLE_PASSWORD_TEXT
def isReadOnlyTextArea(self, obj):
"""Returns True if obj is a text entry area that is read only."""
if not obj.getRole() == pyatspi.ROLE_ENTRY:
return False
state = obj.getState()
readOnly = state.contains(pyatspi.STATE_FOCUSABLE) \
and not state.contains(pyatspi.STATE_EDITABLE)
details = debug.getAccessibleDetails(debug.LEVEL_ALL, obj)
debug.println(debug.LEVEL_ALL,
"Gecko - isReadOnlyTextArea=%s for %s" \
% (readOnly, details))
return readOnly
def nodeLevel(self, obj):
""" Determines the level of at which this object is at by using
the object attribute 'level'. To be consistent with the default
nodeLevel() this value is 0-based (Gecko return is 1-based) """
if obj is None or obj.getRole() == pyatspi.ROLE_HEADING \
or (obj.parent and obj.parent.getRole() == pyatspi.ROLE_MENU):
return -1
try:
state = obj.getState()
except:
return -1
else:
if state.contains(pyatspi.STATE_DEFUNCT):
# Yelp (or perhaps the work-in-progress a11y patch)
# seems to be guilty of this.
#
#print "nodeLevel - obj is defunct", obj
debug.println(debug.LEVEL_WARNING,
"nodeLevel - obj is defunct")
debug.printStack(debug.LEVEL_WARNING)
return -1
attrs = obj.getAttributes()
if attrs is None:
return -1
for attr in attrs:
if attr.startswith("level:"):
return int(attr[6:]) - 1
return -1
def showingDescendants(self, parent):
"""Given an accessible object, returns a list of accessible children
that are actually showing/visible/pursable for flat review. We're
overriding the default method here primarily to handle enormous
tree tables (such as the Thunderbird message list) which do not
manage their descendants.
Arguments:
- parent: The accessible which manages its descendants
Returns a list of Accessible descendants which are showing.
"""
if not parent:
return []
# If this object is not a tree table, if it manages its descendants,
# or if it doesn't have very many children, let the default script
# handle it.
#
if parent.getRole() != pyatspi.ROLE_TREE_TABLE \
or parent.getState().contains(pyatspi.STATE_MANAGES_DESCENDANTS) \
or parent.childCount <= 50:
return script_utilities.Utilities.showingDescendants(self, parent)
try:
table = parent.queryTable()
except NotImplementedError:
return []
descendants = []
# First figure out what columns are visible as there's no point
# in examining cells which we know won't be visible.
#
visibleColumns = []
for i in range(table.nColumns):
header = table.getColumnHeader(i)
if self.pursueForFlatReview(header):
visibleColumns.append(i)
descendants.append(header)
if not len(visibleColumns):
return []
# Now that we know in which columns we can expect to find visible
# cells, try to quickly locate a visible row.
#
startingRow = 0
# If we have one or more selected items, odds are fairly good
# (although not guaranteed) that one of those items happens to
# be showing. Failing that, calculate how many rows can fit in
# the exposed portion of the tree table and scroll down.
#
selectedRows = table.getSelectedRows()
for row in selectedRows:
acc = table.getAccessibleAt(row, visibleColumns[0])
if self.pursueForFlatReview(acc):
startingRow = row
break
else:
try:
tableExtents = parent.queryComponent().getExtents(0)
acc = table.getAccessibleAt(0, visibleColumns[0])
cellExtents = acc.queryComponent().getExtents(0)
except:
pass
else:
rowIncrement = max(1, tableExtents.height / cellExtents.height)
for row in range(0, table.nRows, rowIncrement):
acc = table.getAccessibleAt(row, visibleColumns[0])
if acc and self.pursueForFlatReview(acc):
startingRow = row
break
# Get everything after this point which is visible.
#
for row in range(startingRow, table.nRows):
acc = table.getAccessibleAt(row, visibleColumns[0])
if self.pursueForFlatReview(acc):
descendants.append(acc)
for col in visibleColumns[1:len(visibleColumns)]:
descendants.append(table.getAccessibleAt(row, col))
else:
break
# Get everything before this point which is visible.
#
for row in range(startingRow - 1, -1, -1):
acc = table.getAccessibleAt(row, visibleColumns[0])
if self.pursueForFlatReview(acc):
thisRow = [acc]
for col in visibleColumns[1:len(visibleColumns)]:
thisRow.append(table.getAccessibleAt(row, col))
descendants[0:0] = thisRow
else:
break
return descendants
def uri(self, obj):
"""Return the URI for a given link object.
Arguments:
- obj: the Accessible object.
"""
# Getting a link's URI requires a little workaround due to
# https://bugzilla.mozilla.org/show_bug.cgi?id=379747. You
# should be able to use getURI() directly on the link but
# instead must use ihypertext.getLink(0) on parent then use
# getURI on returned ihyperlink.
try:
ihyperlink = obj.parent.queryHypertext().getLink(0)
except:
return None
else:
try:
return ihyperlink.getURI(0)
except:
return None
#########################################################################
# #
# Utilities for working with the accessible text interface #
# #
#########################################################################
def isWordMisspelled(self, obj, offset):
"""Identifies if the current word is flagged as misspelled by the
application.
Arguments:
- obj: An accessible which implements the accessible text interface.
- offset: Offset in the accessible's text for which to retrieve the
attributes.
Returns True if the word is flagged as misspelled.
"""
attributes, start, end = self.textAttributes(obj, offset, True)
error = attributes.get("invalid")
return error == "spelling"
def setCaretOffset(self, obj, characterOffset):
self._script.setCaretPosition(obj, characterOffset)
self._script.updateBraille(obj)
def textAttributes(self, acc, offset, get_defaults=False):
"""Get the text attributes run for a given offset in a given accessible
Arguments:
- acc: An accessible.
- offset: Offset in the accessible's text for which to retrieve the
attributes.
- get_defaults: Get the default attributes as well as the unique ones.
Default is True
Returns a dictionary of attributes, a start offset where the attributes
begin, and an end offset. Returns ({}, 0, 0) if the accessible does not
supprt the text attribute.
"""
# For really large objects, a call to getAttributes can take up to
# two seconds! This is a Gecko bug. We'll try to improve things
# by storing attributes.
#
attrsForObj = self._script.currentAttrs.get(hash(acc)) or {}
if attrsForObj.has_key(offset):
return attrsForObj.get(offset)
attrs = script_utilities.Utilities.textAttributes(
self, acc, offset, get_defaults)
self._script.currentAttrs[hash(acc)] = {offset:attrs}
return attrs
#########################################################################
# #
# Miscellaneous Utilities #
# #
#########################################################################