Index: src/orca/default.py
===================================================================
--- src/orca/default.py	(revision 3201)
+++ src/orca/default.py	(working copy)
@@ -742,7 +742,7 @@
             self.noOp
         listeners["object:active-descendant-changed"]       = \
             self.onActiveDescendantChanged
-        listeners["object:children-changed:"]               = \
+        listeners["object:children-changed"]               = \
             self.noOp
         listeners["object:link-selected"]                   = \
             self.onLinkSelected
Index: src/orca/liveregions.py
===================================================================
--- src/orca/liveregions.py	(revision 0)
+++ src/orca/liveregions.py	(revision 0)
@@ -0,0 +1,218 @@
+import bisect
+import gobject
+import time
+import traceback
+import pyatspi
+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
+
+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, data, priority):
+        """ Add a new element to the queue according to 1) priority and
+        2) timestamp. """
+        bisect.insort_left(self.queue, (priority, time.time(), data))
+
+    def dequeue(self):
+        """get the highest priority element from the queue.  """
+        return self.queue.pop(0)[2]
+
+    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.msg_queue = PriorityQueue()
+        self._script = script
+        self._priorityOverrides = self._initOverrides()
+
+    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:
+            # we won't do anything for bad markup right now
+            return
+        elif livetype == LIVE_POLITE:
+            self.msg_queue.enqueue(self._getUtterances(event), livetype)
+        elif livetype ==  LIVE_ASSERTIVE:
+            self.msg_queue.purgeByPriority(LIVE_POLITE)
+            self.msg_queue.enqueue(self._getUtterances(event), livetype)
+        elif livetype == LIVE_RUDE:
+            # TODO: Is clearing the queue really necessary?
+            self.msg_queue.clear()
+            speech.stop()
+            speech.speakUtterances(self._getUtterances(event))
+
+    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():
+            utts = self.msg_queue.dequeue()
+            speech.speakUtterances(utts)
+        
+        # See you again soon
+        return True
+        
+    def advanceOverride(self, obj):
+        utterances = []
+        attrs = self._getAttrDictionary(obj)
+
+        # Should probably never see this situation because ids are used be
+        # 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: TODO
+            #
+            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._priorityOverrides.has_key((uri, attrs['id'])):
+                cur_priority = self._priorityOverrides[(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._priorityOverrides[(uri, attrs['id'])] = LIVE_POLITE
+            utterances.append(_('setting live region to polite'))
+        elif cur_priority == LIVE_POLITE:
+            self._priorityOverrides[(uri, attrs['id'])] = LIVE_ASSERTIVE
+            utterances.append(_('setting live region to assertive'))
+        elif cur_priority == LIVE_ASSERTIVE:
+            self._priorityOverrides[(uri, attrs['id'])] = LIVE_RUDE
+            utterances.append(_('setting live region to rude'))
+        elif cur_priority == LIVE_RUDE:
+            self._priorityOverrides[(uri, attrs['id'])] = LIVE_OFF
+            utterances.append(_('setting live region to off'))
+
+        speech.speakUtterances(utterances)
+
+    def _getUtterances(self, event):
+        # currently, the only utterance is the text at the given charoffset.
+        # 
+        if event.type.startswith('object:children-changed:add'):            
+            liveobj = event.any_data
+        else:
+            liveobj = event.source
+
+        utterances = []
+        attrs = self._getAttrDictionary(event.source)
+
+        # Get a handle to the Text interface
+        try:
+            itext = liveobj.queryText()
+        except NotImplementedError:
+            return utterances
+        
+        # Get the message content for the event.
+
+        for relation in liveobj.getRelationSet():
+            if relation.getRelationType() == pyatspi.RELATION_LABELLED_BY:
+                labelobj = relation.getTarget(0)
+                utterances.append(itext.getText(0,-1))
+
+        # Get the text based on the atomic property
+        try:
+            if attrs['container-atomic'] == 'true':
+                utterances.append(itext.getText(0,-1))
+            else:
+                utterances.append(itext.getText(0,-1)) #event.detail1,-1))
+        except KeyError:
+            utterances.append(itext.getText(0,-1))
+        return utterances 
+
+    def _getLiveType(self, obj, attributes=None):
+        attrs = self._getAttrDictionary(obj)
+        uri = self._script.bookmarks._getURIKey()
+ 
+        # look to see if there is a user politeness override
+        try:
+            return self._priorityOverrides[(uri, attrs['id'])]
+        except KeyError:
+            # exception could be thrown due to no override or no 'id'.  In 
+            # either case, just pass to logic below.
+            pass
+    
+        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 _getAttrDictionary(self, obj):
+        return dict([attr.split(':', 1) for attr in obj.getAttributes()])
+
+    def _initOverrides(self):
+        return {}
Index: src/orca/Gecko.py
===================================================================
--- src/orca/Gecko.py	(revision 3201)
+++ src/orca/Gecko.py	(working copy)
@@ -31,6 +31,7 @@
 
 import atk
 import gtk
+import gobject
 import math
 import pyatspi
 import urlparse
@@ -41,6 +42,7 @@
 import default
 import input_event
 import keybindings
+import liveregions as live
 import mag
 import orca
 import orca_state
@@ -126,6 +128,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,
@@ -1744,6 +1749,9 @@
              Script.goPreviousBlockquote,
              Script.goNextTable,
              Script.goPreviousTable,
+             Script.goNextLiveRegion,
+             Script.goPreviousLiveRegion,
+             Script.advanceLivePriority,
              Script.goCellLeft,
              Script.goCellRight,
              Script.goCellUp,
@@ -1800,6 +1808,11 @@
         # keep track if one has appeared or disappeared.
         #
         self._autocompleteVisible = False
+
+        # Create the live region manager and start the message manager
+        self.liveMngr = live.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.
@@ -2173,6 +2186,28 @@
                 #
                 _("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["advanceLivePriority"] = \
+            input_event.InputEventHandler(
+                Script.advanceLivePriority,
+                # Translators: this is for advancing the live regions 
+                # politeness setting
+                #
+                _("Advance live region politeness setting."))
+
         self.inputEventHandlers["toggleCaretNavigationHandler"] = \
             input_event.InputEventHandler(
                 Script.toggleCaretNavigation,
@@ -2225,6 +2260,10 @@
             self.onDocumentLoadStopped
         listeners["object:visible-data-changed"]            = \
             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.
@@ -2653,6 +2692,33 @@
 
         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["advanceLivePriority"]))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
                 "F12",
                 1 << settings.MODIFIER_ORCA,
                 1 << settings.MODIFIER_ORCA,
@@ -3517,6 +3583,23 @@
         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.
         #
@@ -3907,6 +3990,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
@@ -4693,7 +4782,17 @@
                 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.
 
@@ -8539,6 +8638,73 @@
             #
             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
+          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 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 advanceLivePriority(self, inputEvent):
+        
+        if liveRegionsOn:
+            self.liveMngr.advanceOverride(orca_state.locusOfFocus)
+        else:
+            speech.speak(_("Live region support is off"))
+
     def toggleCaretNavigation(self, inputEvent):
         """Toggles between Firefox native and Orca caret navigation."""
 
@@ -8625,3 +8791,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 3201)
+++ 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 \
