Index: src/orca/Gecko.py
===================================================================
--- src/orca/Gecko.py	(revision 2306)
+++ src/orca/Gecko.py	(working copy)
@@ -308,7 +308,7 @@
         # If there's no text for the link, expose part of the
         # link to the user if the image is in a link.
         #
-        link = self._script.getContainingLink(obj)
+        link = self._script.getContainingRole(obj, rolenames.ROLE_LINK)
         if len(text) == 0:
             if link:
                 [linkRegions, focusedRegion] = \
@@ -547,7 +547,7 @@
             # If there's no text for the image, expose the link to
             # the user if the image is in a link.
             #
-            link = self._script.getContainingLink(obj)
+            link = self._script.getContainingRole(obj, rolenames.ROLE_LINK)
             if not len(utterances):
                 if link:
                     utterances.extend(self._getSpeechForLink(link,
@@ -785,6 +785,14 @@
         #
         self._documentFrameCaretContext = {}
 
+        # When navigating in a non-uniform table, one can move to a
+        # cell which spans multiple rows and/or columns.  When moving
+        # beyond that cell, into a cell that does NOT span multiple
+        # rows/columns, we want to be sure we land in the right place.
+        # Therefore, we'll store the coordinates from "our perspective."
+        #
+        self.lastTableCell = [-1, -1]
+
     def getBrailleGenerator(self):
         """Returns the braille generator for this script.
         """
@@ -1971,7 +1979,8 @@
         # the image focus and announce it.
         #
         if event.source.role == rolenames.ROLE_LINK:
-            containingLink = self.getContainingLink(orca_state.locusOfFocus)
+            containingLink = self.getContainingRole(orca_state.locusOfFocus, 
+                                                    rolenames.ROLE_LINK)
             if containingLink == event.source:
                 return
             elif event.source.childCount == 1:
@@ -2315,7 +2324,7 @@
                 if obj.role == rolenames.ROLE_LINK:
                     link = obj
                 else:
-                    link = self.getContainingLink(obj)
+                    link = self.getContainingRole(obj, rolenames.ROLE_LINK)
                 if link:
                     regions.append(braille.Region(
                         " " + rolenames.getBrailleForRoleName(link)))
@@ -2940,35 +2949,54 @@
         return None
 
     def getCellCoordinates(self, obj):
-        """Returns the [row, col] of a ROLE_TABLE_CELL or [0, 0] if this
+        """Returns the [row, col] of a ROLE_TABLE_CELL or [0, 0]
         if the coordinates cannot be found.
         """
 
-        # [[[TODO: WDW - we do this here because Gecko's table interface
-        # is broken and does not give us valid answers.  So...we hack
-        # and try to find the coordinates in other ways.  We assume
-        # the cells are laid out in row/column order, thus allowing us
-        # to do some easy math to find the coordinated.]]]
-        #
         parent = obj.parent
         if parent and parent.table:
-            # row = obj.index / parent.table.nColumns
-            # col = obj.index % parent.table.nColumns
-            # return [row, col]
-            #
-            # By the way, here's the proper way to do it, since we
-            # cannot always depend upon the ordering of the children.
-            # Easy if it works...
-            #
-            # NOTE from Joanie:  Seems like the proper way works now.
-            # Leaving the above in for now until we know for certain.
-            #
             row = parent.table.getRowAtIndex(obj.index)
             col = parent.table.getColumnAtIndex(obj.index)
             return [row, col]
 
         return [0, 0]
 
+    def isSameCell(self, obj, coordinates1, coordinates2):
+        """Returns True if coordinates1 and coordinates2 refer to the
+        same cell in the specified table.
+
+        Arguments:
+        - obj: the table in which to compare the coordinates
+        - coordinates1: [row, col]
+        - coordinates2: [row, col]
+        """
+
+        if not obj.table:
+            return False
+
+        index1 = obj.table.getIndexAt(coordinates1[0], coordinates1[1])
+        index2 = obj.table.getIndexAt(coordinates2[0], coordinates2[1])
+        return (index1 == index2)
+
+    def isNonUniformTable(self, obj):
+        """Returns True if the obj is a non-uniform table (i.e. a table
+        where at least one cell spans multiple rows and/or columns).
+
+        Arguments:
+        - obj: the table to examine
+        """
+
+        if not obj.table:
+            return False
+
+        for i in range(0, obj.childCount):
+            [isCell, row, col, rowExtents, colExtents, isSelected] = \
+                                       obj.table.getRowColumnExtentsAtIndex(i)
+            if (rowExtents > 1) or (colExtents > 1):
+                return True
+
+        return False
+
     def isHeader(self, obj):
         """Returns True if the table cell is a header"""
 
@@ -3044,6 +3072,40 @@
 
         return colHeader
 
+    def getCellSpanInfo(self, obj):
+        """Returns a string reflecting the number of rows and/or columns
+        spanned by a table cell when multiple rows and/or columns are spanned.
+        """
+
+        if obj.role != rolenames.ROLE_TABLE_CELL:
+            return
+
+        [row, col] = self.getCellCoordinates(obj)
+        rowspan = obj.parent.table.getRowExtentAt(row, col)
+        colspan = obj.parent.table.getColumnExtentAt(row, col)
+        spanString = None
+        if (colspan > 1) and (rowspan > 1):
+            # Translators: The cell here refers to a cell within an HTML
+            # table.  We need to announce when the cell occupies or "spans"
+            # more than a single row and/or column.
+            #
+            spanString = _("Cell spans %d rows and %d columns") % \
+                          (rowspan, colspan)
+        elif (colspan > 1):
+            # Translators: The cell here refers to a cell within an HTML
+            # table.  We need to announce when the cell occupies or "spans"
+            # more than a single row and/or column.
+            #
+            spanString = _("Cell spans %d columns") % colspan
+        elif (rowspan > 1):
+            # Translators: The cell here refers to a cell within an HTML
+            # table.  We need to announce when the cell occupies or "spans"
+            # more than a single row and/or column.
+            #
+            spanString = _("Cell spans %d rows") % rowspan
+
+        return spanString
+
     def getTableCaption(self, obj):
         """Returns the ROLE_CAPTION object of a ROLE_TABLE object or None
         if the caption cannot be found.
@@ -3082,26 +3144,28 @@
 
         return basename
 
-    def getContainingLink(self, obj):
-        """Returns the link containing the given object, or None if the
-        given object is not in a link."""
+    def getContainingRole(self, obj, role):
+        """Returns the object of the specified role which contains the
+        given object, or None if the given object is not contained within
+        an object the specified role.
+        """
 
         if not obj:
             return None
 
-        linkObj = None
+        containingObj = None
 
         obj = obj.parent
         while obj and (obj != obj.parent):
-            if obj.role == rolenames.ROLE_LINK:
-                linkObj = obj
+            if obj.role == role:
+                containingObj = obj
                 break
             elif obj.role == rolenames.ROLE_DOCUMENT_FRAME:
                 break
             else:
                 obj = obj.parent
 
-        return linkObj
+        return containingObj
 
     def isUselessObject(self, obj):
         """Returns true if the given object is an obj that doesn't
@@ -3127,7 +3191,7 @@
                     useless = True
 
         if useless:
-            link = self.getContainingLink(obj)
+            link = self.getContainingRole(obj, rolenames.ROLE_LINK)
             if link:
                 useless = False
 
@@ -3949,6 +4013,13 @@
 
         self.caretContext = [obj, characterOffset]
 
+        # If we're not in a table cell, reset self.lastTableCell. 
+        #
+        if obj.role != rolenames.ROLE_TABLE_CELL:
+            cell = self.getContainingRole(obj, rolenames.ROLE_TABLE_CELL)
+            if not cell:
+                self.lastTableCell = [-1, -1]
+
         # Reset focus if need be.
         #
         if obj != orca_state.locusOfFocus:
@@ -4574,7 +4645,7 @@
         # it else we'll get stuck.
         #
         [obj, characterOffset] = self.getCaretContext()
-        containingLink = self.getContainingLink(obj)
+        containingLink = self.getContainingRole(obj, rolenames.ROLE_LINK)
         if containingLink:
             obj = containingLink
 
@@ -4624,7 +4695,7 @@
         # it else we'll get stuck.
         #
         [obj, characterOffset] = self.getCaretContext()
-        containingLink = self.getContainingLink(obj)
+        containingLink = self.getContainingRole(obj, rolenames.ROLE_LINK)
         if containingLink:
             obj = containingLink
 
@@ -4670,6 +4741,19 @@
     def goPreviousTable(self, inputEvent):
         obj = self.findPreviousRole([rolenames.ROLE_TABLE])
         if obj:
+            caption = self.getTableCaption(obj)
+            if caption and caption.text:
+                text = self.getDisplayedText(caption)
+                speech.speak(text)
+            nonUniformString = ""
+            nonUniform = self.isNonUniformTable(obj)
+            if nonUniform:
+                # Translators: a uniform table is one in which each table
+                # cell occupies one row and one column (i.e. a perfect grid)
+                # In contrast, a non-uniform table is one in which at least
+                # one table cell occupies more than one row and/or column.
+                #
+                nonUniformString = _("Non-uniform")
             nRows = obj.table.nRows
             nColumns = obj.table.nColumns
             # Translators: this represents the number of rows in an HTML table.
@@ -4682,12 +4766,10 @@
             colString = ngettext("%d column",
                                  "%d columns",
                                   nColumns) % nColumns
-            speech.speak(rowString + " " + colString)
-            caption = self.getTableCaption(obj)
-            if caption and caption.text:
-                speech.speak(caption.text.getText(0, -1))
+            speech.speak(nonUniformString + " " + rowString + " " + colString)
             cell = obj.table.getAccessibleAt(0, 0)
             obj = atspi.Accessible.makeAccessible(cell)
+            spanString = self.getCellSpanInfo (obj)
             [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
             self.setCaretPosition(obj, characterOffset)
             self.updateBraille(obj)
@@ -4700,6 +4782,9 @@
                 # user has navigated to an empty line.
                 #
                 speech.speak(_("blank"))
+            if spanString:
+                speech.speak(spanString)
+
         else:
             # Translators: this is for navigating HTML content by
             # moving from table to table.
@@ -4709,6 +4794,19 @@
     def goNextTable(self, inputEvent):
         obj = self.findNextRole([rolenames.ROLE_TABLE])
         if obj:
+            caption = self.getTableCaption(obj)
+            if caption and caption.text:
+                text = self.getDisplayedText(caption)
+                speech.speak(text)
+            nonUniformString = ""
+            nonUniform = self.isNonUniformTable(obj)
+            if nonUniform:
+                # Translators: a uniform table is one in which each table
+                # cell occupies one row and one column (i.e. a perfect grid)
+                # In contrast, a non-uniform table is one in which at least
+                # one table cell occupies more than one row and/or column.
+                #
+                nonUniformString = _("Non-uniform")
             nRows = obj.table.nRows
             nColumns = obj.table.nColumns
             # Translators: this represents the number of rows in an HTML table.
@@ -4721,12 +4819,10 @@
             colString = ngettext("%d column",
                                  "%d columns",
                                   nColumns) % nColumns
-            speech.speak(rowString + " " + colString)
-            caption = self.getTableCaption(obj)
-            if caption and caption.text:
-                speech.speak(caption.text.getText(0, -1))
+            speech.speak(nonUniformString + " " + rowString + " " + colString)
             cell = obj.table.getAccessibleAt(0, 0)
             obj = atspi.Accessible.makeAccessible(cell)
+            spanString = self.getCellSpanInfo (obj)
             [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
             self.setCaretPosition(obj, characterOffset)
             self.updateBraille(obj)
@@ -4739,6 +4835,8 @@
                 # user has navigated to an empty line.
                 #
                 speech.speak(_("blank"))
+            if spanString:
+                speech.speak(spanString)
         else:
             # Translators: this is for navigating HTML content by
             # moving from table to table.
@@ -4747,22 +4845,35 @@
 
     def goCellLeft(self, inputEvent):
         [obj, characterOffset] = self.getCaretContext()
-        while obj and (obj != obj.parent) and \
-              (obj.role != rolenames.ROLE_TABLE_CELL):
-            obj = obj.parent
-        if obj and (obj.role == rolenames.ROLE_TABLE_CELL):
+        if obj.role != rolenames.ROLE_TABLE_CELL:
+            obj = self.getContainingRole(obj, rolenames.ROLE_TABLE_CELL)
+        if obj:
             [row, col] = self.getCellCoordinates(obj)
+            [storedRow, storedCol] = self.lastTableCell
+            oldColHeader = self.getColumnHeader(obj)
+            table = obj.parent
+            if self.isSameCell(table, [row, col], [storedRow, storedCol]):
+                # The stored row helps us maintain the correct position
+                # when traversing cells that span multiple rows.
+                #
+                row = storedRow
             if col > 0:
-                cell = obj.parent.table.getAccessibleAt(row, col - 1)
+                cell = table.table.getAccessibleAt(row, col - 1)
+                self.lastTableCell = [row, col - 1]
                 obj = atspi.Accessible.makeAccessible(cell)
+                spanString = self.getCellSpanInfo (obj)
                 [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
                 colHeader = self.getColumnHeader(obj)
-                if colHeader and not self.isHeader(obj):
+                if colHeader and not self.isHeader(obj) and \
+                   (oldColHeader != colHeader):
                     speech.speak(colHeader)
                 self.setCaretPosition(obj, characterOffset)
                 self.updateBraille(obj)
                 self.speakContents( \
                         self.getObjectContentsAtOffset(obj, characterOffset))
+                if spanString:
+                    speech.speak(spanString)
+
             else:
                 # Translators: this is for navigating HTML content by
                 # moving from table cell to table cell.
@@ -4776,22 +4887,37 @@
 
     def goCellRight(self, inputEvent):
         [obj, characterOffset] = self.getCaretContext()
-        while obj and (obj != obj.parent) and \
-              (obj.role != rolenames.ROLE_TABLE_CELL):
-            obj = obj.parent
-        if obj and (obj.role == rolenames.ROLE_TABLE_CELL):
+        if obj.role != rolenames.ROLE_TABLE_CELL:
+            obj = self.getContainingRole(obj, rolenames.ROLE_TABLE_CELL)
+        if obj:
             [row, col] = self.getCellCoordinates(obj)
-            if col < obj.parent.table.nColumns - 1:
-                cell = obj.parent.table.getAccessibleAt(row, col + 1)
+            [storedRow, storedCol] = self.lastTableCell
+            oldColHeader = self.getColumnHeader(obj)
+            table = obj.parent
+            if self.isSameCell(table, [row, col], [storedRow, storedCol]):
+                # The stored row helps us maintain the correct position
+                # when traversing cells that span multiple rows.
+                #
+                row = storedRow
+            colspan = table.table.getColumnExtentAt(row, col)
+            nextCol = col + colspan
+            if nextCol <= table.table.nColumns - 1:
+                cell = table.table.getAccessibleAt(row, nextCol)
+                self.lastTableCell = [row, nextCol]
                 obj = atspi.Accessible.makeAccessible(cell)
+                spanString = self.getCellSpanInfo (obj)
                 colHeader = self.getColumnHeader(obj)
-                if colHeader and not self.isHeader(obj):
+                if colHeader and not self.isHeader(obj) and \
+                   (oldColHeader != colHeader):
                     speech.speak(colHeader)
                 [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
                 self.setCaretPosition(obj, characterOffset)
                 self.updateBraille(obj)
                 self.speakContents( \
                         self.getObjectContentsAtOffset(obj, characterOffset))
+                if spanString:
+                    speech.speak(spanString)
+                    
             else:
                 # Translators: this is for navigating HTML content by
                 # moving from table cell to table cell.
@@ -4805,22 +4931,35 @@
 
     def goCellUp(self, inputEvent):
         [obj, characterOffset] = self.getCaretContext()
-        while obj and (obj != obj.parent) and \
-              (obj.role != rolenames.ROLE_TABLE_CELL):
-            obj = obj.parent
-        if obj and (obj.role == rolenames.ROLE_TABLE_CELL):
+        if obj.role != rolenames.ROLE_TABLE_CELL:
+            obj = self.getContainingRole(obj, rolenames.ROLE_TABLE_CELL)
+        if obj:
             [row, col] = self.getCellCoordinates(obj)
+            [storedRow, storedCol] = self.lastTableCell
+            oldRowHeader = self.getRowHeader(obj)
+            table = obj.parent
+            if self.isSameCell(table, [row, col], [storedRow, storedCol]):
+                # The stored column helps us maintain the correct position
+                # when traversing cells that span multiple columns.
+                #
+                col = storedCol
             if row > 0:
-                cell = obj.parent.table.getAccessibleAt(row - 1, col)
+                cell = table.table.getAccessibleAt(row - 1, col)
+                self.lastTableCell = [row - 1, col]
                 obj = atspi.Accessible.makeAccessible(cell)
+                spanString = self.getCellSpanInfo (obj)
                 rowHeader = self.getRowHeader(obj)
-                if rowHeader and not self.isHeader(obj):
+                if rowHeader and not self.isHeader(obj)and \
+                   (oldRowHeader != rowHeader):
                     speech.speak(rowHeader)
                 [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
                 self.setCaretPosition(obj, characterOffset)
                 self.updateBraille(obj)
                 self.speakContents( \
                         self.getObjectContentsAtOffset(obj, characterOffset))
+                if spanString:
+                    speech.speak(spanString)
+
             else:
                 # Translators: this is for navigating HTML content by
                 # moving from table cell to table cell.
@@ -4834,22 +4973,37 @@
 
     def goCellDown(self, inputEvent):
         [obj, characterOffset] = self.getCaretContext()
-        while obj and (obj != obj.parent) and \
-              (obj.role != rolenames.ROLE_TABLE_CELL):
-            obj = obj.parent
-        if obj and (obj.role == rolenames.ROLE_TABLE_CELL):
+        if obj.role != rolenames.ROLE_TABLE_CELL:
+            obj = self.getContainingRole(obj, rolenames.ROLE_TABLE_CELL)
+        if obj:
             [row, col] = self.getCellCoordinates(obj)
-            if row < obj.parent.table.nRows - 1:
-                cell = obj.parent.table.getAccessibleAt(row + 1, col)
+            [storedRow, storedCol] = self.lastTableCell
+            oldRowHeader = self.getRowHeader(obj)
+            table = obj.parent
+            if self.isSameCell(table, [row, col], [storedRow, storedCol]):
+                # The stored column helps us maintain the correct position
+                # when traversing cells that span multiple columns.
+                #
+                col = storedCol
+            rowspan = table.table.getRowExtentAt(row, col)
+            nextRow = row + rowspan
+            if nextRow <= table.table.nRows - 1:
+                cell = table.table.getAccessibleAt(nextRow, col)
+                self.lastTableCell = [nextRow, col]
                 obj = atspi.Accessible.makeAccessible(cell)
+                spanString = self.getCellSpanInfo (obj)
                 rowHeader = self.getRowHeader(obj)
-                if rowHeader and not self.isHeader(obj):
+                if rowHeader and not self.isHeader(obj)and \
+                   (oldRowHeader != rowHeader):
                     speech.speak(rowHeader)
                 [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
                 self.setCaretPosition(obj, characterOffset)
                 self.updateBraille(obj)
                 self.speakContents( \
                         self.getObjectContentsAtOffset(obj, characterOffset))
+                if spanString:
+                    speech.speak(spanString)
+
             else:
                 # Translators: this is for navigating HTML content by
                 # moving from table cell to table cell.
@@ -4863,12 +5017,13 @@
 
     def goCellFirst(self, inputEvent):
         [obj, characterOffset] = self.getCaretContext()
-        while obj and (obj != obj.parent) and \
-              (obj.role != rolenames.ROLE_TABLE):
-            obj = obj.parent
-        if obj and (obj.role == rolenames.ROLE_TABLE):
+        if obj.role != rolenames.ROLE_TABLE:
+            obj = self.getContainingRole(obj, rolenames.ROLE_TABLE)
+        if obj:
             cell = obj.table.getAccessibleAt(0, 0)
+            self.lastTableCell = [0, 0]
             obj = atspi.Accessible.makeAccessible(cell)
+            spanString = self.getCellSpanInfo (obj)
             [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
             if obj:
                 [row, col] = self.getCellCoordinates(obj)
@@ -4880,6 +5035,8 @@
                 self.updateBraille(obj)
                 self.speakContents( \
                         self.getObjectContentsAtOffset(obj, characterOffset))
+                if spanString:
+                    speech.speak(spanString)
 
         else:
             # Translators: this is for navigating HTML content by
@@ -4889,13 +5046,15 @@
 
     def goCellLast(self, inputEvent):
         [obj, characterOffset] = self.getCaretContext()
-        while obj and (obj != obj.parent) and \
-              (obj.role != rolenames.ROLE_TABLE):
-            obj = obj.parent
-        if obj and (obj.role == rolenames.ROLE_TABLE):
-            cell = obj.table.getAccessibleAt(obj.table.nRows - 1,
-                                             obj.table.nColumns - 1)
+        if obj.role != rolenames.ROLE_TABLE:
+            obj = self.getContainingRole(obj, rolenames.ROLE_TABLE)
+        if obj:
+            lastRow = obj.table.nRows - 1
+            lastCol = obj.table.nColumns - 1
+            cell = obj.table.getAccessibleAt(lastRow, lastCol)
+            self.lastTableCell = [lastRow, lastCol]
             obj = atspi.Accessible.makeAccessible(cell)
+            spanString = self.getCellSpanInfo (obj)
             [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
             if obj:
                 [row, col] = self.getCellCoordinates(obj)
@@ -4907,6 +5066,8 @@
                 self.updateBraille(obj)
                 self.speakContents( \
                         self.getObjectContentsAtOffset(obj, characterOffset))
+                if spanString:
+                    speech.speak(spanString)
 
         else:
             # Translators: this is for navigating HTML content by
