[orca] Additional work on Label Inference with a focus on WebKitGtk.



commit 3b22762306486aa594f816b68fafda526d70f44c
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Sat Dec 10 22:43:17 2011 -0500

    Additional work on Label Inference with a focus on WebKitGtk.

 src/orca/label_inference.py                        |  200 ++++++++++++++++----
 .../scripts/toolkits/WebKitGtk/script_utilities.py |    3 +-
 2 files changed, 161 insertions(+), 42 deletions(-)
---
diff --git a/src/orca/label_inference.py b/src/orca/label_inference.py
index 7cd0c13..8bee14c 100644
--- a/src/orca/label_inference.py
+++ b/src/orca/label_inference.py
@@ -39,6 +39,7 @@ class LabelInference:
         """
 
         self._script = script
+        self._lines = {}
 
     def infer(self, obj, focusedOnly=True):
         """Attempt to infer the functional/displayed label of obj.
@@ -67,12 +68,54 @@ class LabelInference:
             result = obj.description
         if result:
             result = result.strip()
+        self.clearCache()
 
         return result
 
+    def clearCache(self):
+        """Dumps whatever we've stored for performance purposes."""
+
+        self._lines = {}
+
+    def _preferRight(self, obj):
+        """Returns True if we should prefer text on the right, rather than the
+        left, for the object obj."""
+
+        onRightRoles = [pyatspi.ROLE_CHECK_BOX, pyatspi.ROLE_RADIO_BUTTON]
+        return obj.getRole() in onRightRoles
+
+    def _isSimpleObject(self, obj):
+        """Returns True if the given object has 'simple' contents, such as text
+        without embedded objects or a single embedded object without text."""
+
+        if not obj:
+            return False
+
+        children = [child for child in obj]
+        children = filter(lambda x: x.getRole() != pyatspi.ROLE_LINK, children)
+        if len(children) > 1:
+            return False
+
+        if self._isWidget(obj):
+            return False
+
+        try:
+            text = obj.queryText()
+        except NotImplementedError:
+            return True
+
+        string = text.getText(0, -1).decode('UTF-8')
+        if string.find(self._script.EMBEDDED_OBJECT_CHARACTER) > -1:
+            return len(string) == 1
+
+        return True
+
     def _isWidget(self, obj):
         """Returns True if the given object is a widget."""
 
+        if not obj:
+            return False
+
         widgetRoles = [pyatspi.ROLE_CHECK_BOX,
                        pyatspi.ROLE_RADIO_BUTTON,
                        pyatspi.ROLE_COMBO_BOX,
@@ -84,7 +127,7 @@ class LabelInference:
 
         return obj.getRole() in widgetRoles
 
-    def _getExtents(self, obj, startOffset, endOffset):
+    def _getExtents(self, obj, startOffset=0, endOffset=-1):
         """Returns (x, y, width, height) of the text at the given offsets
         if the object implements accessible text, or just the extents of
         the object if it doesn't implement accessible text."""
@@ -110,19 +153,40 @@ class LabelInference:
 
         return extents
 
+    def _createLabelFromContents(self, obj):
+        """Gets the functional label text associated with the object obj."""
+
+        if not self._isSimpleObject(obj):
+            return ''
+
+        contents = self._script.utilities.getObjectsFromEOCs(obj)
+        objects = [content[0] for content in contents]
+        if filter(self._isWidget, objects):
+            return ''
+
+        strings = [content[3] or content[0].name for content in contents]
+        strings = map(lambda x: x.strip(), strings)
+        return ' '.join(strings)
+
     def _getLineContents(self, obj):
         """Get the (obj, startOffset, endOffset, string) tuples for the line
         containing the object, obj."""
 
-        boundary = pyatspi.TEXT_BOUNDARY_LINE_START
-        contents = self._script.utilities.getObjectsFromEOCs(obj, boundary)
-        content = filter(lambda o: o[0] == obj, contents)
-        if content == contents:
+        rv = self._lines.get(hash(obj))
+        if rv:
+            return rv
+
+        key = hash(obj)
+        start = None
+        if self._isWidget(obj):
             start, end = self._script.utilities.getHyperlinkRange(obj)
-            contents = self._script.utilities.getObjectsFromEOCs(
-                obj.parent, boundary, start)
+            obj = obj.parent
+
+        boundary = pyatspi.TEXT_BOUNDARY_LINE_START
+        rv = self._script.utilities.getObjectsFromEOCs(obj, boundary, start)
+        self._lines[key] = rv
 
-        return contents
+        return rv
 
     def inferFromLine(self, obj, proximity=75):
         """Attempt to infer the functional/displayed label of obj by
@@ -135,6 +199,7 @@ class LabelInference:
         Returns the text which we think is the label, or None.
         """
 
+        extents = self._getExtents(obj)
         contents = self._getLineContents(obj)
         content = filter(lambda o: o[0] == obj, contents)
         try:
@@ -142,57 +207,110 @@ class LabelInference:
         except IndexError:
             index = len(contents)
 
-        onLeft = contents[0:index]
-        onLeft.reverse()
-        try:
-            onRight = contents[index+1:]
-        except IndexError:
-            onRight = [()]
-
-        # Normally widgets do not label other widgets.
+        onLeft = contents[max(0, index-1):index]
         onLeft = filter(lambda o: not self._isWidget(o[0]), onLeft)
-        onRight = filter(lambda o: not self._isWidget(o[0]), onRight)
-
-        # Proximity matters, both in terms of objects and of pixels.
-        if onLeft:
-            onLeft = onLeft[0]
-        if onRight:
-            onRight = onRight[0]
-        extents = self._getExtents(obj, 0, 1)
-
-        # Sometimes we should prefer what's to the right of the widget.
-        onRightRoles = [pyatspi.ROLE_CHECK_BOX, pyatspi.ROLE_RADIO_BUTTON]
-        preferRight = obj.getRole() in onRightRoles
-
-        lObj = rObj = None
-        lString = rString = ''
-        lDistance = rDistance = -1
-
         if onLeft:
-            lObj, lStart, lEnd, lString = onLeft
+            lObj, lStart, lEnd, lString = onLeft[0]
             lString = (lString or lObj.name).strip()
             if lString:
                 lExtents = self._getExtents(lObj, lStart, lEnd)
                 lDistance = extents[0] - (lExtents[0] + lExtents[2])
-                if not preferRight and lDistance <= proximity:
+                if not self._preferRight(obj) and lDistance <= proximity:
                     return lString
 
+        onRight = contents[min(len(contents), index+1):]
+        onRight = filter(lambda o: not self._isWidget(o[0]), onRight)
         if onRight:
-            rObj, rStart, rEnd, rString = onRight
+            rObj, rStart, rEnd, rString = onRight[0]
             rString = (rString or rObj.name).strip()
             if rString:
                 rExtents = self._getExtents(rObj, rStart, rEnd)
                 rDistance = rExtents[0] - (extents[0] + extents[2])
-                if preferRight and rDistance <= proximity:
+                if self._preferRight(obj) or rDistance <= proximity:
                     return rString
 
-        if rString and rDistance <= proximity:
-            return rString
-
         return None
 
     def inferFromTable(self, obj):
-        pass
+        """Attempt to infer the functional/displayed label of obj by looking
+        at the contents of the surrounding table cells. Note that this approach
+        assumes a simple table in which the widget is the sole occupant of its
+        cell.
+
+        Arguments
+        - obj: the unlabeled widget
+
+        Returns the text which we think is the label, or None.
+        """
+
+        pred = lambda x: x.getRole() == pyatspi.ROLE_TABLE_CELL
+        cell = pyatspi.utils.findAncestor(obj, pred)
+        if not self._isSimpleObject(cell):
+            return None
+
+        pred = lambda x: x.getRole() == pyatspi.ROLE_TABLE
+        grid = pyatspi.utils.findAncestor(cell, pred)
+        if not grid:
+            return None
+
+        try:
+            table = grid.queryTable()
+        except NotImplementedError:
+            return None
+
+        index = self._script.utilities.cellIndex(cell)
+        row = table.getRowAtIndex(index)
+        col = table.getColumnAtIndex(index)
+
+        if col > 0 and not self._preferRight(obj):
+            candidate = table.getAccessibleAt(row, col - 1)
+            label = self._createLabelFromContents(candidate)
+            if label:
+                return label
+
+        if col < table.nColumns:
+            candidate = table.getAccessibleAt(row, col + 1)
+            label = self._createLabelFromContents(candidate)
+            if label:
+                return label
+
+        if row > 0:
+            candidate = table.getAccessibleAt(row - 1, col)
+            label = self._createLabelFromContents(candidate)
+            if label:
+                return label
+
+        if row < table.nRows:
+            candidate = table.getAccessibleAt(row + 1, col)
+            label = self._createLabelFromContents(candidate)
+            if label:
+                return label
+
+        # None of the cells immediately surrounding this cell seem to be serving
+        # as a functional label. Therefore, see if this table looks like a grid
+        # of widgets with the functional labels in the first row.
+        firstRow = [table.getAccessibleAt(0, i) for i in range(table.nColumns)]
+        if not firstRow or filter(self._isWidget, firstRow):
+            return None
+
+        cells = [table.getAccessibleAt(i, col) for i in range(1, table.nRows)]
+        if filter(lambda x: x[0] and x[0].getRole() != obj.getRole(), cells):
+            return None
+
+        label = self._createLabelFromContents(firstRow[col])
+        if label:
+            return label
+
+        return None
 
     def inferFromOtherLines(self, obj):
+        """Attempt to infer the functional/displayed label of obj by
+        looking at the contents of the previous and/or next line.
+
+        Arguments
+        - obj: the unlabeled widget
+
+        Returns the text which we think is the label, or None.
+        """
+
         pass
diff --git a/src/orca/scripts/toolkits/WebKitGtk/script_utilities.py b/src/orca/scripts/toolkits/WebKitGtk/script_utilities.py
index 4a1624d..195d22d 100644
--- a/src/orca/scripts/toolkits/WebKitGtk/script_utilities.py
+++ b/src/orca/scripts/toolkits/WebKitGtk/script_utilities.py
@@ -137,7 +137,7 @@ class Utilities(script_utilities.Utilities):
         if offset == None:
             offset = text.caretOffset
         if boundary == None:
-            start = offset
+            start = 0
             end = text.characterCount
         else:
             if boundary == pyatspi.TEXT_BOUNDARY_CHAR:
@@ -158,5 +158,6 @@ class Utilities(script_utilities.Utilities):
             objects.append((objs[i], first, last, ''))
             start = last
         objects.append((obj, start, end, string[start:end]))
+        objects = filter(lambda x: x[1] < x[2], objects)
 
         return objects



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]