Index: src/orca/Gecko.py
===================================================================
--- src/orca/Gecko.py	(revision 2288)
+++ src/orca/Gecko.py	(working copy)
@@ -43,6 +43,7 @@
 import settings
 import speech
 import speechgenerator
+import speechserver
 
 from orca_i18n import _
 from orca_i18n import ngettext  # for ngettext support
@@ -1564,6 +1565,106 @@
                 consumes = handler != None
         return consumes
 
+    def textLines(self, obj):
+        """Creates a generator that can be used to iterate over each line
+        of a text object, starting at the caret offset.
+
+        Arguments:
+        - obj: an Accessible that has a text specialization
+
+        Returns an iterator that produces elements of the form:
+        [SayAllContext, acss], where SayAllContext has the text to be
+        spoken and acss is an ACSS instance for speaking the text.
+        """
+
+        [obj, characterOffset] = self.getCaretContext()
+        if obj.text:
+            # Attempt to locate the start of the current sentence by
+            # searching to the left for a sentence terminator.  If we don't
+            # find one, we'll just start the sayAll from at the beginning of
+            # this line/object.
+            #
+            [line, startOffset, endOffset] = \
+                obj.text.getTextAtOffset(
+                                 characterOffset,
+                                 atspi.Accessibility.TEXT_BOUNDARY_LINE_START)
+            if line:
+                terminators = ['. ', '? ', '! ']
+                beginAt = 0
+                for terminator in terminators:
+                    try:
+                        index = line.rindex(terminator,
+                                            0,
+                                            characterOffset - startOffset)
+                        if index > beginAt:
+                            beginAt = index
+                    except:
+                        pass
+                characterOffset = startOffset + beginAt
+
+        done = False
+        while not done:
+            contents = self.getObjectContentsAtOffset(obj, characterOffset)
+            utterances = self.getUtterancesFromContents(contents)
+            clumped = self.clumpUtterances(utterances)
+            for i in range(0, (len(clumped))):
+                [obj, startOffset, endOffset] = \
+                                             contents[min(i, len(contents)-1)]
+                [string, voice] = clumped[i]
+                yield [speechserver.SayAllContext(obj, string,
+                                                  startOffset, endOffset),
+                       voice]
+
+            moreLines = False
+            # getObjectContentsAtOffset() gave us all of the descendants of
+            # the last object.  We need to be sure that we don't "find" one
+            # of those children with findNextObject().
+            #
+            if obj.childCount:
+                obj = obj.child(obj.childCount - 1)
+            while obj and not moreLines:
+                obj = self.findNextObject(obj)
+                if obj:
+                    if obj.role in [rolenames.ROLE_LIST,
+                                    rolenames.ROLE_LIST_ITEM]:
+                        # Adjust the offset so that the item number is spoken.
+                        #
+                        offset = -1
+                    else:
+                        offset = 0
+                    [obj, characterOffset] = \
+                              self.findFirstCaretContext(obj, offset)
+                    moreLines = True
+
+            if not moreLines:
+                done = True
+
+    def __sayAllProgressCallback(self, context, type):
+        if type == speechserver.SayAllContext.PROGRESS:
+            #print "PROGRESS", context.utterance, context.currentOffset
+            #
+            # Attempt to keep the content visible on the screen as
+            # it is being read, but avoid links as grabFocus sometimes
+            # makes them disappear and sayAll to subsequently stop.
+            #
+            if context.currentOffset == 0 and \
+               context.obj.role in [rolenames.ROLE_HEADING,
+                                    rolenames.ROLE_SECTION,
+                                    rolenames.ROLE_PARAGRAPH]:
+                characterCount = context.obj.text.characterCount
+                self.setCaretPosition(context.obj, characterCount-1)
+        elif type == speechserver.SayAllContext.INTERRUPTED:
+            #print "INTERRUPTED", context.utterance, context.currentOffset
+            offset = min(context.currentOffset, len(context.utterance)-1)
+            self.setCaretPosition(context.obj, offset)
+            self.updateBraille(context.obj)
+        elif type == speechserver.SayAllContext.COMPLETED:
+            #print "COMPLETED", context.utterance, context.currentOffset
+            orca.setLocusOfFocus(None, context.obj, False)
+            offset = min(context.currentOffset, len(context.utterance)-1)
+            self.setCaretPosition(context.obj, offset)
+            self.updateBraille(context.obj)
+
     def onCaretMoved(self, event):
         """Caret movement in Gecko is somewhat unreliable and
         unpredictable, but we need to handle it.  When we detect caret
@@ -3615,9 +3716,9 @@
         text = self.getUnicodeText(obj)
         for offset in range(characterOffset, len(text)):
             if text[offset] == self.EMBEDDED_OBJECT_CHARACTER:
-                contents.extend(self.getObjectContentsAtOffset(
-                    obj.child(self.getChildIndex(obj, offset)),
-                    0))
+                child = obj.child(self.getChildIndex(obj, offset))
+                if child:
+                    contents.extend(self.getObjectContentsAtOffset(child, 0))
             elif len(contents):
                 [currentObj, startOffset, endOffset] = contents[-1]
                 if obj == currentObj:
@@ -3697,11 +3798,14 @@
 
         return utterances
 
-    def speakContents(self, contents, speakRole=True):
-        utterances = self.getUtterancesFromContents(contents, speakRole)
+    def clumpUtterances(self, utterances):
+        """Returns a list of utterances clumped together by acss.
 
-        # Now...clump utterances together by acss.
-        #
+        Arguments:
+        -utterances: unclumped utterances
+        -speakRole: if True, speak the roles of objects
+        """
+
         clumped = []
 
         for [string, acss] in utterances:
@@ -3717,11 +3821,18 @@
                 # Translators: "blank" is a short word to mean the
                 # user has navigated to an empty line.
                 #
-                speech.speak(_("blank"), clumped[0][1], False)
-        else:
-            for [string, acss] in clumped:
-                speech.speak(string, acss, False)
+                return [[_("blank"), clumped[0][1]]]
 
+        return clumped
+
+    def speakContents(self, contents, speakRole=True):
+        """Speaks each string in contents using the associated voice/acss"""
+
+        utterances = self.getUtterancesFromContents(contents, speakRole)
+        clumped = self.clumpUtterances(utterances)
+        for [string, acss] in clumped:
+            speech.speak(string, acss, False)
+
     def speakCharacterAtOffset(self, obj, characterOffset):
         """Speaks the character at the given characterOffset in the
         given object."""
