orca r3495 - in trunk: . src/orca
- From: shaeger svn gnome org
- To: svn-commits-list gnome org
- Subject: orca r3495 - in trunk: . src/orca
- Date: Thu, 24 Jan 2008 16:53:05 +0000 (GMT)
Author: shaeger
Date: Thu Jan 24 16:53:04 2008
New Revision: 3495
URL: http://svn.gnome.org/viewvc/orca?rev=3495&view=rev
Log:
Fix for bug #505742, Accommodate no ARIA markup for live regions
Modified:
trunk/ChangeLog
trunk/src/orca/Gecko.py
trunk/src/orca/liveregions.py
trunk/src/orca/settings.py
Modified: trunk/src/orca/Gecko.py
==============================================================================
--- trunk/src/orca/Gecko.py (original)
+++ trunk/src/orca/Gecko.py Thu Jan 24 16:53:04 2008
@@ -45,6 +45,7 @@
import math
import pyatspi
import re
+import time
import urlparse
import braille
@@ -149,9 +150,6 @@
# contain to be considered a match in go to next/prev large object
largeObjectTextLength = 75
-# Whether or not Orca should speak live region changes.
-liveRegionsOn = True
-
# Roles that imply their text starts on a new line.
#
NEWLINE_ROLES = [pyatspi.ROLE_PARAGRAPH,
@@ -1399,8 +1397,7 @@
if not doubleClick or statusOrTitle \
or not self._script.inDocumentContent(obj):
where_am_I.WhereAmI.whereAmI(self, obj, doubleClick, statusOrTitle)
- if self._script.isLiveRegion(obj):
- self._script.liveMngr.outputLiveRegionDescription(obj)
+ self._script.liveMngr.outputLiveRegionDescription(obj)
else:
try:
self._collectionPageSummary()
@@ -1786,12 +1783,10 @@
"""Return the object with the given path (relative to the
document frame). """
returnobj = self._script.getDocumentFrame()
-
for childnumber in path:
- next = returnobj[childnumber]
- if next:
- returnobj = next
- else:
+ try:
+ returnobj = returnobj[childnumber]
+ except IndexError:
return None
return returnobj
@@ -1896,6 +1891,7 @@
Script.goPreviousLiveRegion,
Script.goLastLiveRegion,
Script.advanceLivePoliteness,
+ Script.setLivePolitenessOff,
Script.monitorLiveRegions,
Script.reviewLiveAnnouncement,
Script.goCellLeft,
@@ -2402,6 +2398,14 @@
#
_("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,
@@ -2905,6 +2909,13 @@
| 1 << pyatspi.MODIFIER_ALT \
| 1 << pyatspi.MODIFIER_CONTROL),
1 << pyatspi.MODIFIER_SHIFT,
+ self.inputEventHandlers["setLivePolitenessOff"]))
+
+ keyBindings.add(
+ keybindings.KeyBinding(
+ "backslash",
+ fullModMask,
+ orcaShiftModMask,
self.inputEventHandlers["monitorLiveRegions"]))
keyBindings.add(
@@ -3817,10 +3828,10 @@
Arguments:
- event: the Event
"""
-
self._destroyLineCache()
- if liveRegionsOn and self.isLiveRegion(event.source):
+ # handle live region events
+ if self.handleAsLiveRegion(event):
self.liveMngr.handleEvent(event)
return
@@ -3831,37 +3842,14 @@
for addition events often associated with Javascipt insertion. One such
such example would be the programmatic insertion of a tooltip or alert
dialog."""
+ # no need moving forward if we don't have our target.
+ if event.any_data is None:
+ return
- # We will just look at object addition events for now
- if event.type.startswith('object:children-changed:add:system'):
- # no use moving forward if we don't have our target.
- if event.any_data is None:
- return
-
- if liveRegionsOn and self.isLiveRegion(event.source):
- self.liveMngr.handleEvent(event)
- return
-
- newacc = event.any_data
- output = None
- # The addition could be an alert/tooltip, but the xml-role:alert
- # or xml-role:tooltip object may be a child. For performance
- # reasons first look at the object then just look through the
- # objects immediate descendents, not recursively. May need to
- # check xml-roles to make sure it is not a list or other
- # roles with potentially many children.
- #
- if self.isAriaAlert(newacc):
- output = self.expandEOCs(newacc)
- elif newacc.getRole() != pyatspi.ROLE_LIST:
- for child in newacc:
- if self.isAriaAlert(child):
- output = self.expandEOCs(child)
- break
-
- if output:
- speech.speak(output)
- braille.displayMessage(output)
+ # handle live region events
+ if self.handleAsLiveRegion(event):
+ self.liveMngr.handleEvent(event)
+ return
def onDocumentReload(self, event):
"""Called when the reload button is hit for a web page."""
@@ -3877,6 +3865,8 @@
# events from HTML iframes.
#
if event.source.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
+ # Reset the live region manager.
+ self.liveMngr.reset()
self._loadingDocumentContent = False
def onDocumentLoadStopped(self, event):
@@ -3893,10 +3883,8 @@
Arguments:
- event: the Event
"""
- # We ignore these because Gecko just happily keeps generating
- # name changed events for objects whose name don't change.
- #
- return
+ if event.source.getRole() == pyatspi.ROLE_FRAME:
+ self.liveMngr.flushMessages()
def onFocus(self, event):
"""Called whenever an object gets focus.
@@ -5174,15 +5162,6 @@
except (KeyError, TypeError):
return True
- def isLiveRegion(self, obj):
- attrs = obj.getAttributes()
- if attrs is None:
- return False
- for attr in attrs:
- if attr.startswith('container-live:'):
- return True
- return False
-
def isAriaWidget(self, obj=None):
"""Returns True if the object being examined is an ARIA widget.
@@ -5211,6 +5190,76 @@
return True
return False
+ 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 settings.inferLiveRegions \
+ or not event.type.endswith(':system'):
+ 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'):
+ # This will filter out lists that are not of interest and
+ # events from other tabs.
+ stateset = event.source.getState()
+ if stateset.contains(pyatspi.STATE_FOCUSABLE) \
+ or stateset.contains(pyatspi.STATE_FOCUSED) \
+ or not stateset.contains(pyatspi.STATE_VISIBLE):
+ return False
+
+ # Now we need to look at the object attributes
+ attrs = self._getAttrDictionary(event.source)
+ # Good live region markup
+ if attrs.has_key('container-live'):
+ return True
+
+ # We see this one with the URL bar opening (sometimes)
+ if attrs.has_key('tag') and attrs['tag'] == 'xul:richlistbox':
+ return False
+
+ # This eliminates all ARIA widgets that are not considered live
+ if attrs.has_key('xml-roles') \
+ and not attrs.has_key('container-live'):
+ return False
+
+ else: # object:text-inserted event
+ # 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_FOCUSED) \
+ or not stateset.contains(pyatspi.STATE_VISIBLE):
+ return False
+
+ attrs = self._getAttrDictionary(event.source)
+ # Good live region markup
+ if attrs.has_key('container-live'):
+ return True
+
+ # This might be too restrictive but we need it to filter
+ # out URLs that are displayed when the location list opens.
+ if attrs.has_key('tag') \
+ and attrs['tag'] == 'xul:description' \
+ or attrs['tag'] == 'xul:label':
+ return False
+
+ # This eliminates all ARIA widgets that are not considered live
+ if attrs.has_key('xml-roles') \
+ and not attrs.has_key('container-live'):
+ return False
+ # It sure looks like a live region
+ return True
+
def getCharacterOffsetInParent(self, obj):
"""Returns the character offset of the embedded object
character for this object in its parent's accessible text.
@@ -9985,8 +10034,16 @@
speech.speak(_("Not in a table."))
def goNextLiveRegion(self, inputEvent):
+ # First, get any live regions that have been registered as LIVE_NONE
+ # because there is no markup to test for these but we still want to
+ # find them
+ regobjs = self.liveMngr.getLiveNoneObjects()
+ # define our search predicate
+ pred = lambda obj: (self.liveMngr.matchLiveRegion(obj) or obj in regobjs)
+ # start looking
wrap = True
- [obj, wrapped] = self.findNextByPredicate(self.__matchLiveRegion, wrap)
+ [obj, wrapped] = \
+ self.findNextByPredicate(pred, wrap)
if wrapped:
# Translators: when the user is attempting to locate a
# particular object and the bottom of the web page has been
@@ -10017,8 +10074,16 @@
speech.speak(_("No more live regions."))
def goPreviousLiveRegion(self, inputEvent):
+ # First, get any live regions that have been registered as LIVE_NONE
+ # because there is no markup to test for these but we still want to
+ # find them
+ regobjs = self.liveMngr.getLiveNoneObjects()
+ # define our search predicate
+ pred = lambda obj: (self.liveMngr.matchLiveRegion(obj) or obj in regobjs)
+ # start looking
wrap = True
- [obj, wrapped] = self.findPrevByPredicate(self.__matchLiveRegion, wrap)
+ [obj, wrapped] = \
+ self.findPrevByPredicate(pred, wrap)
if wrapped:
# Translators: when the user is attempting to locate a
# particular object and the bottom of the web page has been
@@ -10047,7 +10112,7 @@
speech.speak(_("No more live regions."))
def goLastLiveRegion(self, inputEvent):
- if liveRegionsOn:
+ if settings.inferLiveRegions:
self.liveMngr.goLastLiveRegion()
else:
# Translators: this announces to the user that live region
@@ -10057,7 +10122,7 @@
def advanceLivePoliteness(self, inputEvent):
"""Advances live region politeness level."""
- if liveRegionsOn:
+ if settings.inferLiveRegions:
self.liveMngr.advancePoliteness(orca_state.locusOfFocus)
else:
# Translators: this announces to the user that live region
@@ -10066,8 +10131,23 @@
speech.speak(_("Live region support is off"))
def monitorLiveRegions(self, inputEvent):
- if liveRegionsOn:
- self.liveMngr.monitorLiveRegions()
+ if not settings.inferLiveRegions:
+ settings.inferLiveRegions = True
+ # Translators: this announces to the user that live region
+ # are being monitored.
+ #
+ speech.speak(_("Live regions monitoring on"))
+ else:
+ settings.inferLiveRegions = False
+ # Translators: this announces to the user that live region
+ # are not being monitored.
+ #
+ self.liveMngr.flushMessages()
+ speech.speak(_("Live regions monitoring off"))
+
+ def setLivePolitenessOff(self, inputEvent):
+ if settings.inferLiveRegions:
+ self.liveMngr.setLivePolitenessOff()
else:
# Translators: this announces to the user that live region
# support has been turned off.
@@ -10075,7 +10155,7 @@
speech.speak(_("Live region support is off"))
def reviewLiveAnnouncement(self, inputEvent):
- if liveRegionsOn:
+ if settings.inferLiveRegions:
self.liveMngr.reviewLiveAnnouncement( \
int(inputEvent.event_string[1:]))
else:
@@ -10171,15 +10251,6 @@
else:
return False
- def __matchLiveRegion(self, obj):
- attrs = obj.getAttributes()
- if attrs is None:
- return False
- for attr in attrs:
- if attr.startswith('live:'):
- return True
- return False
-
def __matchLandmark(self, obj):
if obj is None:
return False
Modified: trunk/src/orca/liveregions.py
==============================================================================
--- trunk/src/orca/liveregions.py (original)
+++ trunk/src/orca/liveregions.py Thu Jan 24 16:53:04 2008
@@ -3,6 +3,8 @@
import time
import pyatspi
import speech
+import settings
+import copy
from orca_i18n import _
@@ -14,7 +16,7 @@
LIVE_RUDE = 3
# Seconds a message is held in the queue before it is discarded
-MSG_KEEPALIVE_TIME = 5 # in seconds
+MSG_KEEPALIVE_TIME = 45 # in seconds
# The number of messages that are cached and can later be reviewed via
# LiveRegionManager.reviewLiveAnnouncement.
@@ -30,10 +32,10 @@
def __init__(self):
self.queue = []
- def enqueue(self, utts, priority, obj):
+ def enqueue(self, data, priority, obj):
""" Add a new element to the queue according to 1) priority and
2) timestamp. """
- bisect.insort_left(self.queue, (priority, time.time(), utts, obj))
+ bisect.insort_left(self.queue, (priority, time.time(), data, obj))
def dequeue(self):
"""get the highest priority element from the queue. """
@@ -56,6 +58,46 @@
myfilter = lambda item: item[0] > priority
self.queue = filter(myfilter, self.queue)
+ def clumpContents(self):
+ """ Combines messages with the same 'label' by appending newer
+ 'content' and removing the newer message. This operation is only
+ applied to the next dequeued message for performance reasons and is
+ often applied in conjunction with filterContents() """
+ if len(self.queue):
+ newqueue = []
+ newqueue.append(self.queue[0])
+ targetlabels = newqueue[0][2]['labels']
+ targetcontent = newqueue[0][2]['content']
+ for i in range(1, len(self.queue)):
+ if self.queue[i][2]['labels'] == targetlabels:
+ newqueue[0][2]['content'].extend(self.queue[i][2]['content'])
+ else:
+ newqueue.append(self.queue[i])
+
+ self.queue = newqueue
+
+ def filterContents(self):
+ """ Combines utterances by eliminating repeated utterances and
+ utterances that are part of other utterances. """
+ if len(self.queue[0][2]['content']) > 1:
+ oldcontent = self.queue[0][2]['content']
+ newcontent = [oldcontent[0]]
+
+ for i in range(1, len(oldcontent)):
+ found = False
+ for j in range(len(newcontent)):
+ if oldcontent[i].find(newcontent[j]) != -1 \
+ or newcontent[j].find(oldcontent[i]) != -1:
+ if len(oldcontent[i]) > len(newcontent[j]):
+ newcontent[j] = oldcontent[i]
+ found = True
+ break
+
+ if not found:
+ newcontent.append(oldcontent[i])
+
+ self.queue[0][2]['content'] = newcontent
+
def __len__(self):
""" Return the length of the queue """
return len(self.queue)
@@ -73,6 +115,7 @@
# User overrides for politeness settings.
self._politenessOverrides = None
+ self._restoreOverrides = None
# last live obj to be announced
self.lastliveobj = None
@@ -93,11 +136,23 @@
script.bookmarks.addSaveObserver(self.bookmarkSaveHandler)
script.bookmarks.addLoadObserver(self.bookmarkLoadHandler)
+ def reset(self):
+ # First we will purge our politeness override dictionary of LIVE_NONE
+ # objects that are not registered for this page
+ newpoliteness = {}
+ currenturi = self._script.bookmarks.getURIKey()
+ for key, value in self._politenessOverrides.iteritems():
+ if key[0] == currenturi or value != LIVE_NONE:
+ newpoliteness[key] = value
+ self._politenessOverrides = newpoliteness
+
def bookmarkSaveHandler(self):
+ """Bookmark save callback"""
self._script.bookmarks.saveBookmarksToDisk(self._politenessOverrides,
filename='politeness')
def bookmarkLoadHandler(self):
+ """Bookmark load callback"""
# readBookmarksFromDisk() returns None on error. Just initialize to an
# empty dictionary if this is the case.
self._politenessOverrides = \
@@ -105,75 +160,90 @@
or {}
def handleEvent(self, event):
-
- livetype = self._getLiveType(event.source)
- # Purge our queue of messages based on priority of incoming event and
- # age of queued message.
- if livetype == LIVE_NONE or livetype == LIVE_OFF:
- # 1) we won't do anything for bad markup right now.
- # 2) we won't waste processing power if live type is lower
- # than user define.
+ """Main live region event handler"""
+ politeness = self._getLiveType(event.source)
+ if politeness == LIVE_OFF:
return
-
- # Add a callback if the queue is empty because we are about to add
- # something to it.
- if len(self.msg_queue) == 0:
- gobject.idle_add(self.pumpMessages)
-
- if livetype == LIVE_POLITE:
+ if politeness == LIVE_NONE:
+ # All the 'registered' LIVE_NONE objects will be set to off
+ # if not monitoring. We will ignore LIVE_NONE objects that
+ # arrive after the user switches off monitoring.
+ if not self.monitoring:
+ return
+ elif politeness == LIVE_POLITE:
# Nothing to do for now
pass
- elif livetype == LIVE_ASSERTIVE:
+ elif politeness == LIVE_ASSERTIVE:
self.msg_queue.purgeByPriority(LIVE_POLITE)
- elif livetype == LIVE_RUDE:
+ elif politeness == LIVE_RUDE:
self.msg_queue.purgeByPriority(LIVE_ASSERTIVE)
- utterance = self._getUtterances(event)
- if utterance:
- self.msg_queue.enqueue(utterance, livetype, event.source)
+ message = self._getMessage(event)
+ if message:
+ if len(self.msg_queue) == 0:
+ gobject.idle_add(self.pumpMessages)
+ self.msg_queue.enqueue(message, politeness, event.source)
def pumpMessages(self):
""" Main gobject callback for live region support. Handles both
purging the message queue and outputting any queued messages that
were queued up in the handleEvent() method.
"""
- # House cleaning on the message queue. No purging is
- # performed in handleEvent().
- self.msg_queue.purgeByKeepAlive()
-
- # If there are messages in the queue and we are not currently
- # speaking then speak queued message.
+ # If there are messages in the queue, we are monitoring, and we are not
+ # currently speaking then speak queued message.
+ # Note: Do all additional work within if statement to prevent
+ # it from being done for each event loop callback
+ # Note: isSpeaking() returns False way too early. A strategy using
+ # a message length (in secs) could be used but don't forget many
+ # parameters such as rate,expanded text and others must be considered.
if len(self.msg_queue) > 0 and not speech.isSpeaking():
- politeness, timestamp, utts, obj = self.msg_queue.dequeue()
+ # House cleaning on the message queue.
+ # First we will purge the queue of old messages
+ self.msg_queue.purgeByKeepAlive()
+ # Next, we will filter the messages
+ self.msg_queue.clumpContents()
+ self.msg_queue.filterContents()
+ # Let's get our queued information
+ politeness, timestamp, message, obj = self.msg_queue.dequeue()
+ # Form output message. No need to repeat labels and content.
+ # TODO: really needs to be tested in real life cases. Perhaps
+ # a verbosity setting?
+ if message['labels'] == message['content']:
+ utts = message['content']
+ else:
+ utts = message['labels'] + message['content']
speech.speakUtterances(utts)
+
# set the last live obj to be announced
self.lastliveobj = obj
+
# cache our message
self._cacheMessage(utts)
+ # We still want to maintain our queue if we are not monitoring
+ if not self.monitoring:
+ self.msg_queue.purgeByKeepAlive()
+
# See you again soon, stay in event loop if we still have messages.
if len(self.msg_queue) > 0:
return True
else:
return False
+
+ def getLiveNoneObjects(self):
+ """Return the live objects that are registered and have a politeness
+ of LIVE_NONE. """
+ retval = []
+ currenturi = self._script.bookmarks.getURIKey()
+ for uri, objectid in self._politenessOverrides:
+ if uri == currenturi and isinstance(objectid, tuple):
+ retval.append(self._script.bookmarks._pathToObj(objectid))
+ return retval
def advancePoliteness(self, obj):
+ """Advance the politeness level of the given object"""
utterances = []
- attrs = self._getAttrDictionary(obj)
-
- # Should probably never see this situation because ids are used by
- # web developers to identify the live region. If we do, the user is
- # out of luck and cannot override politeness level
- if not attrs.has_key('id'):
- # Translators: Objects within webpages sometimes do not have ids.
- # In this rare case, the live politeness property cannot be
- # overriden.
- #
- utterances.append(_('object does not have id'))
- utterances.append(_('cannot override live priority'))
- speech.speakUtterances(utterances)
- return
-
+ objectid = self._getObjectId(obj)
uri = self._script.bookmarks.getURIKey()
try:
@@ -181,30 +251,27 @@
# live property. If an exception is thrown, an override for
# this object has never occurred and the object does not have
# live markup. In either case, set the override to LIVE_NONE.
- if self._politenessOverrides.has_key((uri, attrs['id'])):
- cur_priority = self._politenessOverrides[(uri, attrs['id'])]
- else:
- cur_priority = self._liveStringToType(obj, attributes=attrs)
+ cur_priority = self._politenessOverrides[(uri, objectid)]
except KeyError:
- cur_priority = LIVE_NONE
+ cur_priority = self._liveStringToType(obj)
if cur_priority == LIVE_OFF or cur_priority == LIVE_NONE:
- self._politenessOverrides[(uri, attrs['id'])] = LIVE_POLITE
+ self._politenessOverrides[(uri, objectid)] = LIVE_POLITE
# Translators: sets the live region politeness level to polite
#
utterances.append(_('setting live region to polite'))
elif cur_priority == LIVE_POLITE:
- self._politenessOverrides[(uri, attrs['id'])] = LIVE_ASSERTIVE
+ self._politenessOverrides[(uri, objectid)] = LIVE_ASSERTIVE
# Translators: sets the live region politeness level to assertive
#
utterances.append(_('setting live region to assertive'))
elif cur_priority == LIVE_ASSERTIVE:
- self._politenessOverrides[(uri, attrs['id'])] = LIVE_RUDE
+ self._politenessOverrides[(uri, objectid)] = LIVE_RUDE
# Translators: sets the live region politeness level to rude
#
utterances.append(_('setting live region to rude'))
elif cur_priority == LIVE_RUDE:
- self._politenessOverrides[(uri, attrs['id'])] = LIVE_OFF
+ self._politenessOverrides[(uri, objectid)] = LIVE_OFF
# Translators: sets the live region politeness level to off
#
utterances.append(_('setting live region to off'))
@@ -212,6 +279,8 @@
speech.speakUtterances(utterances)
def goLastLiveRegion(self):
+ """Move the caret to the last announced live region and speak the
+ contents of that object"""
if self.lastliveobj:
self._script.setCaretPosition(self.lastliveobj, 0)
self._script.outlineAccessible(self.lastliveobj)
@@ -219,6 +288,7 @@
self.lastliveobj,0))
def reviewLiveAnnouncement(self, msgnum):
+ """Speak the given number cached message"""
if msgnum > len(self.msg_cache):
# Tranlators: this tells the user that a cached message
# is not available.
@@ -227,50 +297,45 @@
else:
speech.speakUtterances(self.msg_cache[-msgnum])
- def monitorLiveRegions(self):
+ def setLivePolitenessOff(self):
+ """User toggle to set all live regions to LIVE_OFF or back to their
+ original politeness."""
# start at the document frame
- obj = self._script.getDocumentFrame()
+ docframe = self._script.getDocumentFrame()
# get the URI of the page. It is used as a partial key.
uri = self._script.bookmarks.getURIKey()
# The user is currently monitoring live regions but now wants to
# change all live region politeness on page to LIVE_OFF
if self.monitoring:
- # look through all the objects on the page and set/add to
- # politeness overrides.
- # TODO: use Collection
- matches = pyatspi.findAllDescendants(obj, self._livePred)
- for match in matches:
- attrs = self._getAttrDictionary(match)
- self._politenessOverrides[(uri, attrs['id'])] = LIVE_OFF
-
# Translators: This lets the user know that all live regions
# have been turned off.
speech.speak(_("All live regions set to off"))
+ self.msg_queue.clear()
+
+ # First we'll save off a copy for quick restoration
+ self._restoreOverrides = copy.copy(self._politenessOverrides)
+
+ # Set all politeness overrides to LIVE_OFF.
+ for override in self._politenessOverrides.keys():
+ self._politenessOverrides[override] = LIVE_OFF
+
+ # look through all the objects on the page and set/add to
+ # politeness overrides. This only adds live regions with good
+ # markup.
+ matches = pyatspi.findAllDescendants(docframe, self.matchLiveRegion)
+ for match in matches:
+ objectid = self._getObjectId(match)
+ self._politenessOverrides[(uri, objectid)] = LIVE_OFF
+
# Toggle our flag
self.monitoring = False
# The user wants to restore politeness levels
else:
- # Get the politeness bookmarks
- oldpoliteness = \
- self._script.bookmarks.readBookmarksFromDisk( \
- filename='politeness') or {}
-
- # look through all the objects on the page.
- # TODO: use Collection
- matches = pyatspi.findAllDescendants(obj, self._livePred)
- for match in matches:
- attrs = self._getAttrDictionary(match)
- # Restore the bookmarked politeness or the markup politeness
- try:
- self._politenessOverrides[(uri, attrs['id'])] = \
- oldpoliteness[(uri, attrs['id'])]
- except KeyError:
- self._politenessOverrides[(uri, attrs['id'])] = \
- self._liveStringToType(obj, attributes=attrs)
-
+ for key, value in self._restoreOverrides.iteritems():
+ self._politenessOverrides[key] = value
# Translators: This lets the user know that all live regions
# have been restored to their original politeness level.
speech.speak(_("live regions politeness levels restored"))
@@ -279,7 +344,9 @@
self.monitoring = True
def outputLiveRegionDescription(self, obj):
- attrs = self._getAttrDictionary(obj)
+ """Used in conjuction with whereAmI to output description and
+ politeness of the given live region object"""
+ objectid = self._getObjectId(obj)
uri = self._script.bookmarks.getURIKey()
utterances = []
@@ -296,46 +363,44 @@
# get the politeness level as a string
try:
- livepriority = self._politenessOverrides[(uri, attrs['id'])]
+ livepriority = self._politenessOverrides[(uri, objectid)]
liveprioritystr = self._liveTypeToString(livepriority)
except KeyError:
- if attrs.has_key('live'):
- liveprioritystr = attrs['live']
- else:
- liveprioritystr = 'unknown'
+ liveprioritystr = 'none'
- # Translators: output the politeness level
- #
- utterances.append(_('politeness level %s') %liveprioritystr)
- speech.speakUtterances(utterances)
+ # We will only output useful information
+ # TODO: check for repeated descriptions
+ if utterances or liveprioritystr != 'none':
+ # Translators: output the politeness level
+ #
+ utterances.append(_('politeness level %s') %liveprioritystr)
+ speech.speakUtterances(utterances)
- def _livePred(self, obj):
+ def matchLiveRegion(self, obj):
+ """Predicate used to find a live region"""
attrs = self._getAttrDictionary(obj)
- return (attrs.has_key('container-live') and attrs.has_key('id'))
-
- def _getUtterances(self, event):
+ return attrs.has_key('container-live')
+ def _getMessage(self, event):
+ """Gets the message associated with a given live event."""
attrs = self._getAttrDictionary(event.source)
- # TODO: handle relevant property here
-
- # Get the message content for the event. First the relations.
- utterances = []
-
+ content = []
+ labels = []
if event.type.startswith('object:children-changed:add'):
# Get a handle to the Text interface for the target.
try:
targetitext = event.any_data.queryText()
except NotImplementedError:
- return []
+ return None
# Get the text based on the atomic property
try:
if attrs['container-atomic'] == 'true':
- utterances.append(self._script.expandEOCs(event.source))
+ content.append(self._script.expandEOCs(event.source))
else:
- utterances.append(targetitext.getText(0, -1))
+ content.append(targetitext.getText(0, -1))
except (KeyError, TypeError):
- utterances.append(targetitext.getText(0, -1))
+ content.append(targetitext.getText(0, -1))
else: #object:text-changed:insert
# Get a handle to the Text interface for the source.
@@ -344,60 +409,102 @@
try:
sourceitext = event.source.queryText()
except NotImplementedError:
- # TODO: output warning
return None
# We found an embed character. We can expect a children-changed
# event, which we will act on, so just return.
- content = sourceitext.getText(0, -1)
- if content.find(self._script.EMBEDDED_OBJECT_CHARACTER) == 0:
+ txt = sourceitext.getText(0, -1)
+ if txt.find(self._script.EMBEDDED_OBJECT_CHARACTER) == 0:
return None
# Get labeling information
- utterances.extend(self._getLabelsAsUtterances(event.source))
+ labels = self._getLabelsAsUtterances(event.source)
# Get the text based on the atomic property
try:
if attrs['container-atomic'] == 'true':
- utterances.append(content)
+ content.append(txt)
else:
- utterances.append(content[event.detail1:])
+ content.append(txt[event.detail1:])
except KeyError:
- utterances.append(content[event.detail1:])
-
- return utterances
+ content.append(txt)
+
+ return {'content':content, 'labels':labels}
+
+ def flushMessages(self):
+ self.msg_queue.clear()
def _cacheMessage(self, utts):
+ """Cache a message in our cache list of length CACHE_SIZE"""
self.msg_cache.append(utts)
if len(self.msg_cache) > CACHE_SIZE:
self.msg_cache.pop(0)
def _getLabelsAsUtterances(self, obj):
- utterances = []
- for relation in obj.getRelationSet():
- relationtype = relation.getRelationType()
- if relationtype == pyatspi.RELATION_LABELLED_BY:
- labelobj = relation.getTarget(0)
- try:
- utterances.append(labelobj.queryText().getText(0, -1))
- except NotImplemented:
- pass
- return utterances
+ """Get the labels for a given object"""
+ # try the Gecko label getter first
+ uttstring = self._script.getDisplayedLabel(obj)
+ if uttstring:
+ return [uttstring.strip()]
+ # often we see a table cell. I'll implement my own label getter
+ elif obj.getRole() == pyatspi.ROLE_TABLE_CELL \
+ and obj.parent.childCount > 1:
+ # We will try the table interface first for it's parent
+ try:
+ itable = obj.parent.queryTable()
+ # I'm in a table, now what row are we in? Look in the first
+ # columm of that row.
+ #
+ # Note: getRowHeader() fails for most markup. We will use the
+ # relation when the markup is good (when getRowHeader() works)
+ # so we won't see this code in those cases.
+ row = itable.getRowAtIndex(obj.getIndexInParent())
+ header = itable.getAccessibleAt(row, 0)
+ # expand the header
+ return [self._script.expandEOCs(header).strip()]
+ except NotImplementedError:
+ pass
+
+ # Last ditch effort is to see if our parent is a table row <tr>
+ # element.
+ parentattrs = self._getAttrDictionary(obj.parent)
+ if parentattrs.has_key('tag') and parentattrs['tag'] == 'TR':
+ return [self._script.expandEOCs( \
+ obj.parent.getChildAtIndex(0)).strip()]
+
+ # Sorry, no valid labels found
+ return []
def _getLiveType(self, obj):
- attrs = self._getAttrDictionary(obj)
+ """Returns the live politeness setting for a given object. Also,
+ registers LIVE_NONE objects in politeness overrides when monitoring."""
+ objectid = self._getObjectId(obj)
uri = self._script.bookmarks.getURIKey()
-
- # look to see if there is a user politeness override
+ if self._politenessOverrides.has_key((uri, objectid)):
+ # look to see if there is a user politeness override
+ return self._politenessOverrides[(uri, objectid)]
+ else:
+ livetype = self._liveStringToType(obj)
+ # We'll save off a reference to LIVE_NONE if we are monitoring
+ # to give the user a chance to change the politeness level. It
+ # is done here for performance sake (objectid, uri are expensive)
+ if livetype == LIVE_NONE and self.monitoring:
+ self._politenessOverrides[(uri, objectid)] = livetype
+ return livetype
+
+ def _getObjectId(self, obj):
+ """Returns the HTML 'id' or a path to the object is an HTML id is
+ unavailable"""
+ attrs = self._getAttrDictionary(obj)
+ if attrs is None:
+ return self._getPath(obj)
try:
- return self._politenessOverrides[(uri, attrs['id'])]
- except (KeyError, TypeError):
- # exception could be thrown due to no override, no 'id' or
- # attrs being None (unlikely). In any case, just pass to logic
- # below.
- return self._liveStringToType(obj, attributes=attrs)
+ return attrs['id']
+ except KeyError:
+ return self._getPath(obj)
def _liveStringToType(self, obj, attributes=None):
+ """Returns the politeness enum for a given object"""
attrs = attributes or self._getAttrDictionary(obj)
try:
if attrs['container-live'] == 'off':
@@ -413,6 +520,7 @@
return LIVE_NONE
def _liveTypeToString(self, politeness):
+ """Returns the politeness level as a string given a politeness enum"""
if politeness == LIVE_OFF:
return 'off'
elif politeness == LIVE_POLITE:
@@ -427,3 +535,18 @@
def _getAttrDictionary(self, obj):
return dict([attr.split(':', 1) for attr in obj.getAttributes()])
+
+ def _getPath(self, obj):
+ """ Returns, as a tuple of integers, the path from the given object
+ to the document frame."""
+ docframe = self._script.getDocumentFrame()
+ path = []
+ while 1:
+ if obj.parent is None or obj == docframe:
+ path.reverse()
+ return tuple(path)
+ try:
+ path.append(obj.getIndexInParent())
+ except Exception:
+ raise LookupError
+ obj = obj.parent
Modified: trunk/src/orca/settings.py
==============================================================================
--- trunk/src/orca/settings.py (original)
+++ trunk/src/orca/settings.py Thu Jan 24 16:53:04 2008
@@ -899,3 +899,9 @@
# This is a list of events that Orca should immidiately drop and never look at.
#
ignoredEventsList = ['object:bounds-changed']
+
+# Listen to Live Region events. Tells Gecko.onChildrenChanged() and
+# onTextInserted() event handlers to monitor these events for live region
+# changes.
+#
+inferLiveRegions = True
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]