Index: src/orca/liveregions.py
===================================================================
--- src/orca/liveregions.py	(revision 0)
+++ src/orca/liveregions.py	(revision 0)
@@ -0,0 +1,173 @@
+import bisect
+import gobject
+import speech
+import time
+
+import atspi
+
+# 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 a given 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.channels = ['off', 'default']  
+        self.currentChannel = 0  # TODO grab a user pref setting
+        self._script = script        
+
+    def handleEvent(self, event):
+        # some debug info
+        #print 'handleEvent', event.source.role
+
+        if self.channels[self.currentChannel] == 'off':
+            return
+
+        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.
+
+          Return (Boolean): Return True to continue to be included in the
+                              gobject event loop.
+        """
+
+        # Keep all work that impacts performance inside this if statement
+        # this method is called repeatedly!
+        #
+        if self.channels[self.currentChannel] != 'off':
+            # House cleaning on the message queue.  Do this first because 
+            # the channel may have just been turned on.  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 nextChannel(self):
+        self.currentChannel += 1
+        if self.currentChannel >= len(self.channels):
+            self.currentChannel = 0
+
+    def getChannelName(self):
+        return self.channels[self.currentChannel]
+
+    def _getUtterances(self, event):
+        # currently, the only utterance is the text at the given charoffset.
+        # 
+
+        if event.type.startswith('object:children-changed:add'):
+            liveobj = atspi.Accessible.makeAccessible(event.any_data.value())
+        else:
+            liveobj = event.source
+
+        utterances = []
+        attrs = self._getAttrDictionary(event.source)
+
+        relations = liveobj.relations
+        for relation in relations:
+            if relation.getRelationType() == atspi.Accessibility.RELATION_LABELLED_BY:
+                labelobj = atspi.Accessible.makeAccessible(relation.getTarget(0))
+                utterances.append(labelobj.text.getText(0,-1))
+
+        # Get the text based on the atomic property
+        try:
+            if attrs['container-atomic'] == 'true':
+                utterances.append(liveobj.text.getText(0,-1))
+            else:
+                utterances.append(liveobj.text.getText(event.detail1,-1))
+        except KeyError:
+            utterances.append(liveobj.text.getText(0,-1))
+
+        return utterances 
+            
+    def _getLiveType(self, obj):
+        attrs = obj.attributes
+        if attrs is None:
+            return LIVE_NONE
+        for attr in attrs:
+            if attr.startswith('container-live:'):
+                livetype = attr[15:]
+                if livetype == 'polite':
+                    return LIVE_POLITE
+                elif livetype == 'assertive':
+                    return LIVE_ASSERTIVE
+                elif livetype == 'rude':
+                    return LIVE_RUDE
+                else:
+                    return LIVE_NONE
+        return LIVE_NONE
+            
+    def _getAttrDictionary(self, obj):
+        return dict([attr.split(':', 1) for attr in obj.attributes])
+ 
Index: src/orca/Gecko.py
===================================================================
--- src/orca/Gecko.py	(revision 2824)
+++ src/orca/Gecko.py	(working copy)
@@ -39,8 +39,10 @@
 import braillegenerator
 import debug
 import default
+import gobject
 import input_event
 import keybindings
+import liveregions as live
 import mag
 import orca
 import orca_state
@@ -1697,6 +1699,10 @@
         #
         self._objectForFocusGrab = None
         
+        # Create the live region manager and start the message manager
+        self.liveMngr = live.LiveRegionManager(self)
+        gobject.idle_add(self.liveMngr.pumpMessages, priority=gobject.PRIORITY_LOW)
+        
         # We don't want to prevent the user from arrowing into an
         # autocomplete when it appears in a search form.  We need to
         # keep track if one has appeared or disappeared.
@@ -2111,6 +2117,12 @@
                 # interrupted.
                 #
                 _("Speaks entire document."))
+        self.inputEventHandlers["liveChannelChanger"] = \
+            input_event.InputEventHandler(
+                Script.liveChannelChanger,
+                # Translators: 
+                #
+                _("Changes the live region channel."))
                 
         
 
@@ -2127,6 +2139,8 @@
             self.onDocumentLoadStopped
         listeners["object:visible-data-changed"]            = \
             self.onVisibleDataChanged
+        listeners["object:children-changed"]                = \
+            self.onChildrenChanged
 
         # [[[TODO: HACK - WDW we need to accomodate Gecko's incorrect
         # use of underscores instead of dashes until they fix their bug.
@@ -2134,7 +2148,7 @@
         #
         listeners["object:property-change:accessible_value"] = \
             self.onValueChanged
-
+        
         return listeners
 
     def __getArrowBindings(self):
@@ -2573,6 +2587,15 @@
                 1 << settings.MODIFIER_ORCA,
                 1 << settings.MODIFIER_ORCA,
                 self.inputEventHandlers["toggleStructuralNavigationHandler"]))
+                
+        keyBindings.add(
+            keybindings.KeyBinding(
+                "y",
+                (1 << atspi.Accessibility.MODIFIER_SHIFT \
+                 | 1 << atspi.Accessibility.MODIFIER_ALT \
+                 | 1 << atspi.Accessibility.MODIFIER_CONTROL),
+                0,
+                self.inputEventHandlers["liveChannelChanger"]))
 
         if controlCaretNavigation:
             for keyBinding in self.__getArrowBindings().keyBindings:
@@ -3381,6 +3404,9 @@
         # If text is deleted from an object, we want to trash our
         # cache of the unicode text.
         #
+        
+    #    print 'onTextDeleted', event.type, event.source.text.getText(0,-1), event.source.attributes
+        
         try:
             del event.source.unicodeText
         except:
@@ -3417,13 +3443,27 @@
         # If text is inserted into an object, we want to trash our
         # cache of the unicode text.
         #
+       # print 'onTextInserted', event.type, event.source.role, event.source.text.getText(0,-1), event.source.attributes
+        if self.isLiveRegion(event.source):
+            self.liveMngr.handleEvent(event)
+        
         try:
             del event.source.unicodeText
         except:
             pass
 
         default.Script.onTextInserted(self, event)
+        
+    def isLiveRegion(self, obj):
+        attrs = obj.attributes
+        if attrs is None:
+            return False
+        for attr in attrs:
+            if attr.startswith('container-live:'):
+                return True
 
+        return False
+
     def onDocumentReload(self, event):
         """Called when the reload button is hit for a web page."""
         # We care about the main document and we'll ignore document
@@ -3797,6 +3837,12 @@
             self.updateBraille(obj)
             self.speakContents(
                 self.getLineContentsAtOffset(obj, characterOffset))
+                
+    def onChildrenChanged(self, event):
+        if event.type.startswith('object:children-changed:add:system'):
+            if self.isLiveRegion(event.source):
+                
+                self.liveMngr.handleEvent(event)
 
     def handleProgressBarUpdate(self, event, obj):
         """Determine whether this progress bar event should be spoken or not.
@@ -4573,6 +4619,14 @@
             if attr.startswith('xml-roles'):
                 return True
         return False
+        
+    def liveChannelChanger(self, keyboardEvent):
+        self.liveMngr.nextChannel()
+        # Translators: This announces the live region channel that is being 
+        # switched to.
+        #
+        speech.speak(_('live region channel %s') \
+                                          %self.liveMngr.getChannelName())
 
     def getCharacterOffsetInParent(self, obj):
         """Returns the character offset of the embedded object
Index: src/orca/Makefile.am
===================================================================
--- src/orca/Makefile.am	(revision 2824)
+++ src/orca/Makefile.am	(working copy)
@@ -32,6 +32,7 @@
 	J2SE-access-bridge.py \
 	keybindings.py \
 	keynames.py \
+        liveregions.py \
 	mag.py \
 	orca.py \
 	orca_console_prefs.py \
