Index: src/orca/bookmarks.py
===================================================================
--- src/orca/bookmarks.py	(revision 3333)
+++ src/orca/bookmarks.py	(working copy)
@@ -28,14 +28,23 @@
 
 from orca_i18n import _
 
+
 class Bookmarks:
     """Represents a default bookmark handler."""
     def __init__(self, script):
         self._script = script
         self._bookmarks = {} 
+        self._saveObservers = []
+        self._loadObservers = []
         self._loadBookmarks() 
         self._currentbookmarkindex = None
-  
+
+    def addSaveObserver(self, observer):
+        self._saveObservers.append(observer)
+
+    def addLoadObserver(self, observer):
+        self._loadObservers.append(observer)
+
     def goToBookmark(self, inputEvent, index=None):
         """ Go to the bookmark indexed by inputEvent.hw_code """
         # establish the _bookmarks index
@@ -136,6 +145,10 @@
             # disk
             #
             speech.speak(_('bookmarks could not be saved'))
+
+        # Notify the observers
+        for o in self._saveObservers:
+            o()
             
     def goToNextBookmark(self, inputEvent):
         """ Go to the next bookmark location.  If no bookmark has yet to be
@@ -187,24 +200,30 @@
     def _loadBookmarks(self):
         """ Load this scripts saved bookmarks."""
         self._bookmarks = self._readBookmarksFromDisk() or {}
-        
-    def _readBookmarksFromDisk(self):
+
+        # notify the observers
+        for o in self._loadObservers:
+            o()
+
+    def _readBookmarksFromDisk(self, filename=None):
         """ Read saved bookmarks from disk.  Currently an unpickled object
         that represents a bookmark """
+        filename = filename or self._script.name.split(' ')[0]
         orcaDir = settings.userPrefsDir
         orcaBookmarksDir = os.path.join(orcaDir, "bookmarks")
         try:
             inputFile = open( os.path.join( orcaBookmarksDir, \
-                        '%s.pkl' %self._script.name.split(' ')[0]), "r")
+                        '%s.pkl' %filename), "r")
             bookmarks = pickle.load(inputFile)
             inputFile.close()
             return bookmarks
         except (IOError, EOFError, OSError):
             return None
         
-    def _saveBookmarksToDisk(self, bookmarksObj):
+    def _saveBookmarksToDisk(self, bookmarksObj, filename=None):
         """ Write bookmarks to disk.  bookmarksObj must be a pickleable 
         object. """
+        filename = filename or self._script.name.split(' ')[0]
         orcaDir = settings.userPrefsDir
         orcaBookmarksDir = os.path.join(orcaDir, "bookmarks")
         # create directory if it does not exist.  correct place??
@@ -213,7 +232,7 @@
         except OSError:
             os.mkdir(orcaBookmarksDir)
         output = open( os.path.join( orcaBookmarksDir, \
-                    '%s.pkl' %self._script.name.split(' ')[0]), "w", os.O_CREAT)
+                    '%s.pkl' %filename), "w", os.O_CREAT)
         pickle.dump(bookmarksObj, output)
         output.close()
         
Index: src/orca/liveregions.py
===================================================================
--- src/orca/liveregions.py	(revision 0)
+++ src/orca/liveregions.py	(revision 0)
@@ -0,0 +1,406 @@
+import bisect
+import gobject
+import os
+import time
+import traceback
+import pyatspi
+import settings
+import speech
+
+from orca_i18n import _
+
+# define 'live' property types
+LIVE_OFF       = -1
+LIVE_NONE      = 0
+LIVE_POLITE    = 1
+LIVE_ASSERTIVE = 2
+LIVE_RUDE      = 3
+
+# Seconds a message is held in the queue before it is discarded
+MSG_KEEPALIVE_TIME = 5  # in seconds
+
+# The number of messages that are cached and can later be reviewed via 
+# LiveRegionManager.reviewLiveAnnouncement.
+CACHE_SIZE = 9  # corresponds to one of nine key bindings
+
+class PriorityQueue:
+    """ This class represents a thread **UNSAFE** priority queue where priority
+    is determined by the given integer priority.  The entries are also maintained  
+    in chronological order. 
+
+    TODO: experiment with Queue.Queue to make thread safe
+    """
+    def __init__(self):
+        self.queue = []
+
+    def enqueue(self, utts, 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))
+       
+    def dequeue(self):
+        """get the highest priority element from the queue.  """
+        return self.queue.pop(0)
+
+    def clear(self):
+        """ Clear the queue """
+        self.queue = []
+
+    def purgeByKeepAlive(self):
+        """ Purge items from the queue that are older than the keepalive 
+        time """
+        currenttime = time.time()
+        myfilter = lambda item: item[1] + MSG_KEEPALIVE_TIME > currenttime
+        self.queue = filter(myfilter, self.queue)
+
+    def purgeByPriority(self, priority):
+        """ Purge items from the queue that have a lower than or equal priority
+        than the given argument """
+        myfilter = lambda item: item[0] > priority
+        self.queue = filter(myfilter, self.queue)
+
+    def __len__(self):
+        """ Return the length of the queue """
+        return len(self.queue)
+
+
+class LiveRegionManager:
+    def __init__(self, script):
+        self._script = script
+        # message priority queue
+        self.msg_queue = PriorityQueue()
+
+        # Message cache.  Used to store up to 9 previous messages so user can
+        # review if desired.
+        self.msg_cache = []
+
+        # User overrides for politeness settings.
+        self._politenessOverrides = None
+
+        # last live obj to be announced
+        self.lastliveobj = None
+
+        # Used to track whether a user wants to monitor all live regions
+        # Not to be confused with the global Gecko.liveRegionsOn which 
+        # completely turns off live region support.  This one is based on
+        # a user control by changing politeness levels to LIVE_OFF or back
+        # to the bookmark or markup politeness value.
+        self.monitoring = True
+
+        # Set up politeness level overrides and subscribe to bookmarks
+        # for load and save user events.
+        # We are initialized after bookmarks so call the load handler once
+        # to get initialized.
+        #
+        self.bookmarkLoadHandler()
+        script.bookmarks.addSaveObserver(self.bookmarkSaveHandler)
+        script.bookmarks.addLoadObserver(self.bookmarkLoadHandler)
+
+    def bookmarkSaveHandler(self):
+        self._script.bookmarks._saveBookmarksToDisk(self._politenessOverrides, \
+                                                    filename='politeness')
+
+    def bookmarkLoadHandler(self):
+        # _readBookmarksFromDisk() returns None on error. Just initialize to an
+        # empty dictionary if this is the case.
+        self._politenessOverrides = \
+        self._script.bookmarks._readBookmarksFromDisk(filename='politeness') \
+        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:
+            # 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.
+            return
+        elif livetype == LIVE_POLITE:
+            self.msg_queue.enqueue(self._getUtterances(event), livetype, event.source)
+        elif livetype ==  LIVE_ASSERTIVE:
+            self.msg_queue.purgeByPriority(LIVE_POLITE)
+            self.msg_queue.enqueue(self._getUtterances(event), livetype, event.source)
+        elif livetype == LIVE_RUDE:
+            self.msg_queue.purgeByPriority(LIVE_ASSERTIVE)
+            self.msg_queue.enqueue(self._getUtterances(event), livetype, 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 len(self.msg_queue) > 0 and not speech.isSpeaking():
+            politeness, time, utts, obj = self.msg_queue.dequeue()
+            speech.speakUtterances(utts)
+            # set the last live obj to be announced
+            self.lastliveobj = obj
+            # cache our message
+            self._cacheMessage(utts)
+        # See you again soon, stay in event loop
+        return True 
+
+    def advancePoliteness(self, obj):
+        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
+
+        uri = self._script.bookmarks._getURIKey()
+
+        try:
+            # The current priority is either a previous override or the
+            # 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.
+            # TODO: add object properties to output ??  Can this logic be cleaner??
+            if self._politenessOverrides.has_key((uri, attrs['id'])):
+                cur_priority = self._politenessOverrides[(uri, attrs['id'])] 
+            else:
+                cur_priority = self._liveStringToType(obj, attributes=attrs)
+        except KeyError:
+            cur_priority = LIVE_NONE
+                
+        if cur_priority == LIVE_OFF or cur_priority == LIVE_NONE:
+            self._politenessOverrides[(uri, attrs['id'])] = 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
+            # 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
+            # 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
+            # Translators:  sets the live region politeness level to off
+            #
+            utterances.append(_('setting live region to off'))
+
+        speech.speakUtterances(utterances)
+
+    def goLastLiveRegion(self):
+        if self.lastliveobj:
+            self._script.setCaretPosition(self.lastliveobj, 0)
+            self._script.outlineAccessible(self.lastliveobj)
+            self._script.speakContents(self._script.getObjectContentsAtOffset(\
+                                       self.lastliveobj,0))
+                                       
+    def reviewLiveAnnouncement(self, msgnum):
+        if msgnum > len(self.msg_cache):
+            #TODO: Tranlators:
+            speech.speak(_('no live message saved'))
+        else:
+            speech.speakUtterances(self.msg_cache[-msgnum])
+
+    def monitorLiveRegions(self):
+        # start at the document frame
+        obj = 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"))
+
+            # 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)
+
+            # 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"))
+
+            # Toggle our flag
+            self.monitoring = True  
+
+    def outputLiveRegionDescription(self, obj):
+        attrs = self._getAttrDictionary(obj)
+        uri = self._script.bookmarks._getURIKey()
+
+        utterances = []
+
+        # get the description if there is one.
+        for relation in liveobj.getRelationSet():
+            relationtype = relation.getRelationType()
+            if relationtype == pyatspi.RELATION_DESCRIBED_BY:
+                targetobj = relation.getTarget(0)
+                try:
+                    utterances.append(targetobj.queryText().getText(0,-1))
+                except NotImplemented:
+                    pass
+
+        # get the politeness level as a string
+        try:
+            livepriority = self._politenessOverrides[(uri, attrs['id'])]
+            liveprioritystr = self._liveTypeToString(livepriority)
+        except KeyError:
+            if attrs.has_key('live'):
+                liveprioritystr = attrs['live']
+            else:
+                liveprioritystr = 'unknown'
+
+        # Translators: output the politeness level
+        #
+        utterances.append(_('politeness level %s') %liveprioritystr)
+        speech.speakUtterances(utterances)
+
+    def _livePred(self, obj):
+        attrs = self._getAttrDictionary(obj)
+        return (attrs.has_key('container-live') and attrs.has_key('id'))
+
+    def _getUtterances(self, event):
+
+        attrs = self._getAttrDictionary(event.source)
+        # print 'attrs=', attrs
+        
+        # TODO: handle relevant property here
+        
+
+        # Get the message content for the event.  First the relations.
+        utterances = []
+        
+        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 [] 
+            
+            # Get the text based on the atomic property
+            try:
+                if attrs['container-atomic'] == 'true':
+                    utterances.append(self._script.expandEOCs(event.source))
+                else:
+                    utterances.append(targetitext.getText(0,-1))
+            except (KeyError, TypeError):
+                utterances.append(targetitext.getText(0,-1))
+            
+        else: #object:text-changed:insert
+            # Get a handle to the Text interface for the source.
+            # Serious problems if this fails.
+            #
+            try:
+                sourceitext = event.source.queryText()
+            except NotImplementedError:
+                # TODO: output warning
+                return []
+            
+            # Get labeling information
+            utterances.extend(self._getLabelsAsUtterances(event.source))
+
+            # Get the text based on the atomic property
+            try:
+                if attrs['container-atomic'] == 'true':
+                    utterances.append(sourceitext.getText(0,-1))
+                else:
+                    utterances.append(sourceitext.getText(event.detail1,-1))
+            except KeyError:
+                utterances.append(sourceitext.getText(event.detail1,-1))
+
+        return utterances 
+
+    def _cacheMessage(self, utts):
+        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
+
+    def _getLiveType(self, obj):
+        attrs = self._getAttrDictionary(obj)
+        uri = self._script.bookmarks._getURIKey()
+
+        # look to see if there is a user politeness override
+        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)
+
+    def _liveStringToType(self, obj, attributes=None):
+        attrs = attributes or self._getAttrDictionary(obj)
+        try:
+            if attrs['container-live'] == 'off': return LIVE_OFF
+            elif attrs['container-live'] == 'polite':  return LIVE_POLITE
+            elif attrs['container-live'] == 'assertive': return LIVE_ASSERTIVE
+            elif attrs['container-live'] == 'rude': return LIVE_RUDE
+            else: return LIVE_NONE
+        except KeyError:
+            return LIVE_NONE
+        
+    def _liveTypeToString(self, politeness):
+        if politeness == LIVE_OFF: return 'off'
+        elif politeness == LIVE_POLITE: return 'polite'
+        elif politeness == LIVE_ASSERTIVE: return 'assertive'
+        elif politeness == LIVE_RUDE: return 'rude'
+        elif politeness == LIVE_NONE: return 'none'
+        else: return 'unknown'
+
+    def _getAttrDictionary(self, obj):
+        return dict([attr.split(':', 1) for attr in obj.getAttributes()])
Index: src/orca/Gecko.py
===================================================================
--- src/orca/Gecko.py	(revision 3333)
+++ src/orca/Gecko.py	(working copy)
@@ -42,6 +42,7 @@
 
 import atk
 import gtk
+import gobject
 import math
 import pyatspi
 import urlparse
@@ -52,6 +53,7 @@
 import default
 import input_event
 import keybindings
+import liveregions
 import mag
 import orca
 import orca_state
@@ -137,6 +139,9 @@
 # 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,
@@ -1365,9 +1370,12 @@
         """Calls the base class method for single clicks and Gecko specific
         information presentation methods for double clicks
         """
+
         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)
         else:
             try:
                 self._collectionPageSummary()
@@ -1637,6 +1645,10 @@
             # disk
             #
             speech.speak(_('bookmarks could not be saved'))
+
+        # Notify the observers
+        for o in self._saveObservers:
+            o()
             
     def goToNextBookmark(self, inputEvent):
         """ Go to the next bookmark location.  If no bookmark has yet to be
@@ -1853,6 +1865,12 @@
              Script.goPreviousBlockquote,
              Script.goNextTable,
              Script.goPreviousTable,
+             Script.goNextLiveRegion,
+             Script.goPreviousLiveRegion,
+             Script.goLastLiveRegion,
+             Script.advanceLivePoliteness,
+             Script.monitorLiveRegions,
+             Script.reviewLiveAnnouncement,
              Script.goCellLeft,
              Script.goCellRight,
              Script.goCellUp,
@@ -1909,6 +1927,11 @@
         # 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)
+        gobject.idle_add(self.liveMngr.pumpMessages, 
+                         priority=gobject.PRIORITY_DEFAULT_IDLE)
         
     def getWhereAmI(self):
         """Returns the "where am I" class for this script.
@@ -2282,6 +2305,52 @@
                 #
                 _("Goes to next table."))
 
+        self.inputEventHandlers["goPreviousLiveRegion"] = \
+            input_event.InputEventHandler(
+                Script.goPreviousLiveRegion,
+                # Translators: this is for navigating between live regions
+                #
+                _("Goes to previous live region."))
+
+        self.inputEventHandlers["goNextLiveRegion"] = \
+            input_event.InputEventHandler(
+                Script.goNextLiveRegion,
+                # Translators: this is for navigating between live regions
+                #
+                _("Goes to next live region."))
+
+        self.inputEventHandlers["goLastLiveRegion"] = \
+            input_event.InputEventHandler(
+                Script.goLastLiveRegion,
+                # Translators: this is for navigating to the last live region
+                # to make an announcement.
+                #
+                _("Goes to last live region."))
+
+        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["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["toggleCaretNavigationHandler"] = \
             input_event.InputEventHandler(
                 Script.toggleCaretNavigation,
@@ -2336,6 +2405,8 @@
             self.onVisibleDataChanged
         listeners["object:children-changed"]                = \
             self.onChildrenChanged
+        listeners["object:text-changed:insert"]             = \
+            self.onTextInserted
 
         # [[[TODO: HACK - WDW we need to accomodate Gecko's incorrect
         # use of underscores instead of dashes until they fix their bug.
@@ -2687,6 +2758,61 @@
 
         keyBindings.add(
             keybindings.KeyBinding(
+                "r",
+                (1 << pyatspi.MODIFIER_SHIFT \
+                 | 1 << pyatspi.MODIFIER_ALT \
+                 | 1 << pyatspi.MODIFIER_CONTROL),
+                1 << pyatspi.MODIFIER_SHIFT,
+                self.inputEventHandlers["goPreviousLiveRegion"]))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
+                "r",
+                (1 << pyatspi.MODIFIER_SHIFT \
+                 | 1 << pyatspi.MODIFIER_ALT \
+                 | 1 << pyatspi.MODIFIER_CONTROL),
+                0,
+                self.inputEventHandlers["goNextLiveRegion"]))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
+                "y",
+                (1 << pyatspi.MODIFIER_SHIFT \
+                 | 1 << pyatspi.MODIFIER_ALT \
+                 | 1 << pyatspi.MODIFIER_CONTROL),
+                0,
+                self.inputEventHandlers["goLastLiveRegion"]))
+                
+        # keybindings to provide chat room message history.
+        messageKeys = [ "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9" ]
+        for messageKey in messageKeys:
+            keyBindings.add(
+                keybindings.KeyBinding(
+                    messageKey,
+                    1 << settings.MODIFIER_ORCA,
+                    1 << settings.MODIFIER_ORCA,
+                    self.inputEventHandlers["reviewLiveAnnouncement"]))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
+                "backslash",
+                (1 << pyatspi.MODIFIER_SHIFT \
+                 | 1 << pyatspi.MODIFIER_ALT \
+                 | 1 << pyatspi.MODIFIER_CONTROL),
+                1 << pyatspi.MODIFIER_SHIFT,
+                self.inputEventHandlers["monitorLiveRegions"]))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
+                "backslash",
+                (1 << pyatspi.MODIFIER_SHIFT \
+                 | 1 << pyatspi.MODIFIER_ALT \
+                 | 1 << pyatspi.MODIFIER_CONTROL),
+                0,
+                self.inputEventHandlers["advanceLivePoliteness"]))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
                 "F12",
                 fullModMask,
                 orcaModMask,
@@ -3544,6 +3670,22 @@
         Arguments:
         - event: the Event
         """
+        # TODO:  del cache here too???
+        if liveRegionsOn and self.isLiveRegion(event.source):
+            # If a text-changed:insert has an embed character, it will
+            # certainly have an associated object:children-changed event.  For
+            # this case we will simply ignore the text event and act on the
+            # children-changed event.
+            #
+            # TODO: Consider moving this embed character check to 
+            # liveregions._getUtterances for performance reasons.
+            # 
+            content = event.source.queryText().getText(0, -1)
+            if content.find(self.EMBEDDED_OBJECT_CHARACTER) == -1:
+                self.liveMngr.handleEvent(event)
+            # TODO:  let pass or return here??
+            return
+        
         # If text is inserted into an object, we want to trash our
         # cache of the unicode text.
         #
@@ -3969,6 +4111,12 @@
             self.speakContents(
                 self.getLineContentsAtOffset(obj, characterOffset))
 
+    def onChildrenChanged(self, event):
+        if liveRegionsOn \
+              and event.type.startswith('object:children-changed:add:system') \
+              and self.isLiveRegion(event.source):
+            self.liveMngr.handleEvent(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
@@ -4737,7 +4885,8 @@
         if not structuralNavigationEnabled:
             return False
         
-        if self.isAriaWidget():
+        if self.isAriaWidget() \
+                and not self.isLiveRegion(orca_state.locusOfFocus):
             return False
 
         # If the Orca_Modifier key was pressed, we're handling it.
@@ -4764,7 +4913,16 @@
                 obj = obj.parent
 
         return False
-            
+
+    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.
 
@@ -4772,15 +4930,13 @@
         - obj: The accessible object of interest.  If None, the
         locusOfFocus is examined.
         """
-        
         obj = obj or orca_state.locusOfFocus
-        if obj:
-            attrs = obj.getAttributes()
-            for attr in attrs:
-                if attr.startswith('xml-roles'):
-                    return True
-        return False
+        attrs = self._getAttrDictionary(obj)
+        return attrs.has_key('xml-roles') and not attrs.has_key('live')
 
+    def _getAttrDictionary(self, obj):
+        return dict([attr.split(':', 1) for attr in obj.getAttributes()])
+
     def isAriaAlert(self, obj):
         """Returns True if the given object is an ARIA wairole:alert or 
            wairole:tooltip.
@@ -6058,8 +6214,8 @@
             return [None, -1]
 
         text = self.queryNonEmptyText(obj)
-        if text and not (self.isAriaWidget(obj) 
-                         and obj.getRole() == pyatspi.ROLE_LIST):
+        if text  and not (self.isAriaWidget(obj) 
+                          and obj.getRole() == pyatspi.ROLE_LIST):
             unicodeText = self.getUnicodeText(obj)
             nextOffset = startOffset + 1
             while nextOffset < len(unicodeText):
@@ -8636,6 +8792,94 @@
             #
             speech.speak(_("Not in a table."))
 
+    def goNextLiveRegion(self, inputEvent):
+        wrap = True
+        [obj, wrapped] = self.findNextByPredicate(self.__matchLiveRegion, wrap)
+        if wrapped:
+            # Translators: when the user is attempting to locate a
+            # particular object and the bottom of the web page has been
+            # reached without that object being found, we "wrap" to the
+            # top of the page and continuing looking downwards. We need
+            # to inform the user when this is taking place.
+            #
+            speech.speak(_("Wrapping to top."))
+        if obj:
+            # TODO:   We don't want to move to a list item.  
+            # Is this the best place to handle this?
+            if obj.getRole() == pyatspi.ROLE_LIST:
+                characterOffset = 0
+                obj = obj[0]
+            else:
+                [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
+            self.setCaretPosition(obj, characterOffset)
+            self.updateBraille(obj)
+            # For debugging
+            self.outlineAccessible(obj)
+            self.speakContents(self.getObjectContentsAtOffset(obj,
+                                                                characterOffset))
+        else:
+            # Translators: this is for navigating HTML in a structural
+            # manner, where a 'live region' is a location in a web page
+            # that are updated without having to refresh the entire page.
+            #
+            speech.speak(_("No more live regions."))
+
+    def goPreviousLiveRegion(self, inputEvent):
+        wrap = True
+        [obj, wrapped] = self.findPrevByPredicate(self.__matchLiveRegion, wrap)
+        if wrapped:
+            # Translators: when the user is attempting to locate a
+            # particular object and the bottom of the web page has been
+            # reached without that object being found, we "wrap" to the
+            # top of the page and continuing looking downwards. We need
+            # to inform the user when this is taking place.
+            #
+            speech.speak(_("Wrapping to top."))
+        if obj:
+            # TODO:   We don't want to move to a list item.  
+            # Is this the best place to handle this?
+            if obj.getRole() == pyatspi.ROLE_LIST:
+                characterOffset = 0
+            else:
+                [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
+            self.setCaretPosition(obj, characterOffset)
+            self.updateBraille(obj)
+            self.outlineAccessible(obj)
+            self.speakContents(self.getObjectContentsAtOffset(obj,
+                                                                characterOffset))
+        else:
+            # Translators: this is for navigating HTML in a structural
+            # manner, where a 'live region' is a location in a web page
+            # that are updated without having to refresh the entire page.
+            #
+            speech.speak(_("No more live regions."))
+            
+    def goLastLiveRegion(self, inputEvent):
+        if liveRegionsOn:
+            self.liveMngr.goLastLiveRegion()
+        else:
+            speech.speak(_("Live region support is off"))
+
+    def advanceLivePoliteness(self, inputEvent):
+        """Advances live region politeness level."""
+        if liveRegionsOn:
+            self.liveMngr.advancePoliteness(orca_state.locusOfFocus)
+        else:
+            speech.speak(_("Live region support is off"))
+            
+    def monitorLiveRegions(self, inputEvent):
+        if liveRegionsOn:
+            self.liveMngr.monitorLiveRegions()
+        else:
+            speech.speak(_("Live region support is off"))
+
+    def reviewLiveAnnouncement(self, inputEvent):
+        if liveRegionsOn:
+            self.liveMngr.reviewLiveAnnouncement( \
+                                    int(inputEvent.event_string[1:]))
+        else:
+            speech.speak(_("Live region support is off"))
+
     def toggleCaretNavigation(self, inputEvent):
         """Toggles between Firefox native and Orca caret navigation."""
 
@@ -8722,3 +8966,13 @@
                 return False
         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
+
Index: src/orca/Makefile.am
===================================================================
--- src/orca/Makefile.am	(revision 3333)
+++ src/orca/Makefile.am	(working copy)
@@ -31,6 +31,7 @@
 	J2SE-access-bridge.py \
 	keybindings.py \
 	keynames.py \
+	liveregions.py \
 	mag.py \
 	orca.py \
 	orca_console_prefs.py \
