[orca] Continued work and refinement of the label inference code for WebKitGtk content



commit c9b66d64c6134de83e807ddbcee48a09851ebdd0
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Mon Dec 12 12:21:08 2011 -0500

    Continued work and refinement of the label inference code for WebKitGtk content

 src/orca/label_inference.py  |  294 ++++++++++++++++++++++++++++++++++--------
 src/orca/script_utilities.py |    9 +-
 2 files changed, 245 insertions(+), 58 deletions(-)
---
diff --git a/src/orca/label_inference.py b/src/orca/label_inference.py
index 8bee14c..c32d28b 100644
--- a/src/orca/label_inference.py
+++ b/src/orca/label_inference.py
@@ -29,6 +29,8 @@ __license__   = "LGPL"
 
 import pyatspi
 
+import debug
+
 class LabelInference:
 
     def __init__(self, script):
@@ -39,7 +41,9 @@ class LabelInference:
         """
 
         self._script = script
-        self._lines = {}
+        self._lineCache = {}
+        self._extentsCache = {}
+        self._isWidgetCache = {}
 
     def infer(self, obj, focusedOnly=True):
         """Attempt to infer the functional/displayed label of obj.
@@ -51,31 +55,53 @@ class LabelInference:
         Returns the text which we think is the label, or None.
         """
 
-        isFocused = obj.getState().contains(pyatspi.STATE_FOCUSED)
-        if focusedOnly and not isFocused:
+        debug.println(debug.LEVEL_FINE, "INFER label for: %s" % obj)
+        if not obj:
+            return None
+
+        if focusedOnly and not obj.getState().contains(pyatspi.STATE_FOCUSED):
+            debug.println(debug.LEVEL_FINE, "INFER - object not focused")
             return None
 
         result = None
         if not result:
-            result = self.inferFromLine(obj)
+            result = self.inferFromTextLeft(obj)
+            debug.println(debug.LEVEL_FINE, "INFER - Text Left: %s" % result)
+        if not result or self._preferRight(obj):
+            result = self.inferFromTextRight(obj)
+            debug.println(debug.LEVEL_FINE, "INFER - Text Right: %s" % result)
         if not result:
             result = self.inferFromTable(obj)
+            debug.println(debug.LEVEL_FINE, "INFER - Table: %s" % result)
         if not result:
-            result = self.inferFromOtherLines(obj)
+            result = self.inferFromTextAbove(obj)
+            debug.println(debug.LEVEL_FINE, "INFER - Text Above: %s" % result)
+        if not result:
+            result = self.inferFromTextBelow(obj)
+            debug.println(debug.LEVEL_FINE, "INFER - Text Below: %s" % result)
+
+        # TODO - We probably do not wish to "infer" from these. Instead, we
+        # should ensure that this content gets presented as part of the widget.
+        # (i.e. the label is something on screen. Widget name and description
+        # are each something other than a label.)
         if not result:
             result = obj.name
+            debug.println(debug.LEVEL_FINE, "INFER - Name: %s" % result)
         if not result:
             result = obj.description
+            debug.println(debug.LEVEL_FINE, "INFER - Description: %s" % result)
         if result:
             result = result.strip()
-        self.clearCache()
 
+        self.clearCache()
         return result
 
     def clearCache(self):
         """Dumps whatever we've stored for performance purposes."""
 
-        self._lines = {}
+        self._lineCache = {}
+        self._extentsCache = {}
+        self._isWidgetCache = {}
 
     def _preferRight(self, obj):
         """Returns True if we should prefer text on the right, rather than the
@@ -84,6 +110,20 @@ class LabelInference:
         onRightRoles = [pyatspi.ROLE_CHECK_BOX, pyatspi.ROLE_RADIO_BUTTON]
         return obj.getRole() in onRightRoles
 
+    def _preferTop(self, obj):
+        """Returns True if we should prefer text above, rather than below for
+        the object obj."""
+
+        roles = [pyatspi.ROLE_COMBO_BOX, pyatspi.ROLE_LIST]
+
+        # Put new-to-pyatspi roles here.
+        try:
+            roles.append(pyatspi.ROLE_LIST_BOX)
+        except:
+            pass
+
+        return obj.getRole() in roles
+
     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."""
@@ -96,15 +136,12 @@ class LabelInference:
         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')
+        string = text.getText(0, -1).decode('UTF-8').strip()
         if string.find(self._script.EMBEDDED_OBJECT_CHARACTER) > -1:
             return len(string) == 1
 
@@ -116,28 +153,46 @@ class LabelInference:
         if not obj:
             return False
 
+        rv = self._isWidgetCache.get(hash(obj))
+        if rv != None:
+            return rv
+
         widgetRoles = [pyatspi.ROLE_CHECK_BOX,
                        pyatspi.ROLE_RADIO_BUTTON,
+                       pyatspi.ROLE_TOGGLE_BUTTON,
                        pyatspi.ROLE_COMBO_BOX,
-                       pyatspi.ROLE_DOCUMENT_FRAME,
                        pyatspi.ROLE_LIST,
+                       pyatspi.ROLE_MENU,
+                       pyatspi.ROLE_MENU_ITEM,
                        pyatspi.ROLE_ENTRY,
                        pyatspi.ROLE_PASSWORD_TEXT,
                        pyatspi.ROLE_PUSH_BUTTON]
 
-        return obj.getRole() in widgetRoles
+        # Put new-to-pyatspi roles here.
+        try:
+            widgetRoles.append(pyatspi.ROLE_LIST_BOX)
+        except:
+            pass
+
+        isWidget = obj.getRole() in widgetRoles
+        self._isWidgetCache[hash(obj)] = isWidget
+        return isWidget
 
     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."""
 
-        extents = 0, 0, 0, 0
+        if not obj:
+            return 0, 0, 0, 0
 
+        rv = self._extentsCache.get((hash(obj), startOffset, endOffset))
+        if rv:
+            return rv
+
+        extents = 0, 0, 0, 0
         try:
             text = obj.queryText()
-        except AttributeError:
-            return extents
         except NotImplementedError:
             pass
         else:
@@ -145,12 +200,11 @@ class LabelInference:
             if not obj.getRole() in skipTextExtents:
                 extents = text.getRangeExtents(startOffset, endOffset, 0)
 
-        if extents[2] and extents[3]:
-            return extents
-
-        ext = obj.queryComponent().getExtents(0)
-        extents = ext.x, ext.y, ext.width, ext.height
+        if not (extents[2] and extents[3]):
+            ext = obj.queryComponent().getExtents(0)
+            extents = ext.x, ext.y, ext.width, ext.height
 
+        self._extentsCache[(hash(obj), startOffset, endOffset)] = extents
         return extents
 
     def _createLabelFromContents(self, obj):
@@ -159,6 +213,9 @@ class LabelInference:
         if not self._isSimpleObject(obj):
             return ''
 
+        if self._isWidget(obj):
+            return ''
+
         contents = self._script.utilities.getObjectsFromEOCs(obj)
         objects = [content[0] for content in contents]
         if filter(self._isWidget, objects):
@@ -172,7 +229,7 @@ class LabelInference:
         """Get the (obj, startOffset, endOffset, string) tuples for the line
         containing the object, obj."""
 
-        rv = self._lines.get(hash(obj))
+        rv = self._lineCache.get(hash(obj))
         if rv:
             return rv
 
@@ -184,13 +241,27 @@ class LabelInference:
 
         boundary = pyatspi.TEXT_BOUNDARY_LINE_START
         rv = self._script.utilities.getObjectsFromEOCs(obj, boundary, start)
-        self._lines[key] = rv
+        self._lineCache[key] = rv
 
         return rv
 
-    def inferFromLine(self, obj, proximity=75):
+    def _getPreviousObject(self, obj):
+        """Gets the object prior to obj."""
+
+        index = obj.getIndexInParent()
+        if not index > 0:
+            return obj.parent
+
+        prevObj = obj.parent[index-1]
+        if prevObj and prevObj.childCount:
+            prevObj = prevObj[prevObj.childCount - 1]
+
+        return prevObj
+
+    def inferFromTextLeft(self, obj, proximity=75):
         """Attempt to infer the functional/displayed label of obj by
-        looking at the contents of the current line.
+        looking at the contents of the current line, which are to the
+        left of this object
 
         Arguments
         - obj: the unlabeled widget
@@ -208,26 +279,146 @@ class LabelInference:
             index = len(contents)
 
         onLeft = contents[max(0, index-1):index]
-        onLeft = filter(lambda o: not self._isWidget(o[0]), onLeft)
-        if 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 self._preferRight(obj) and lDistance <= proximity:
-                    return lString
+        onLeft = filter(lambda o: o[0] and not self._isWidget(o[0]), onLeft)
+        if not onLeft:
+            return None
+
+        lObj, start, end, string = onLeft[-1]
+        string = (string or lObj.name).strip()
+        if not string:
+            return None
+
+        lExtents = self._getExtents(lObj, start, end)
+        distance = extents[0] - (lExtents[0] + lExtents[2])
+        if distance <= proximity:
+            return string
+
+        return None
+
+    def inferFromTextRight(self, obj, proximity=25):
+        """Attempt to infer the functional/displayed label of obj by
+        looking at the contents of the current line, which are to the
+        right of this object
+
+        Arguments
+        - obj: the unlabeled widget
+        - proximity: pixels expected for a match
+
+        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:
+            index = contents.index(content[0])
+        except IndexError:
+            index = len(contents)
 
         onRight = contents[min(len(contents), index+1):]
-        onRight = filter(lambda o: not self._isWidget(o[0]), onRight)
-        if 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 self._preferRight(obj) or rDistance <= proximity:
-                    return rString
+        onRight = filter(lambda o: o[0] and not self._isWidget(o[0]), onRight)
+        if not onRight:
+            return None
+
+        rObj, start, end, string = onRight[0]
+        string = (string or rObj.name).strip()
+        if not string:
+            return None
+
+        rExtents = self._getExtents(rObj, start, end)
+        distance = rExtents[0] - (extents[0] + extents[2])
+        if distance <= proximity or self._preferRight(obj):
+            return string
+
+        return None
+
+    def inferFromTextAbove(self, obj, proximity=20):
+        """Attempt to infer the functional/displayed label of obj by
+        looking at the contents of the line above the line containing
+        the object obj.
+
+        Arguments
+        - obj: the unlabeled widget
+        - proximity: pixels expected for a match
+
+        Returns the text which we think is the label, or None.
+        """
+
+        thisLine = self._getLineContents(obj)
+        prevObj, start, end, string = thisLine[0]
+        if obj == prevObj:
+            start, end = self._script.utilities.getHyperlinkRange(prevObj)
+            prevObj = prevObj.parent
+
+        try:
+            text = prevObj.queryText()
+        except (AttributeError, NotImplementedError):
+            return None
+
+        objX, objY, objWidth, objHeight = self._getExtents(obj)
+        if not (objWidth and objHeight):
+            return None
+
+        boundary = pyatspi.TEXT_BOUNDARY_LINE_START
+        line = text.getTextBeforeOffset(start, boundary)
+        string = line[0].strip()
+        if string:
+            x, y, width, height = self._getExtents(prevObj, start, end)
+            distance = objY - (y + height)
+            if distance <= proximity:
+                return string
+
+        while prevObj:
+            prevObj = self._getPreviousObject(prevObj)
+            x, y, width, height = self._getExtents(prevObj)
+            distance = objY - (y + height)
+            if distance > proximity:
+                return None
+            if distance < 1:
+                continue
+            if x + 150 < objX:
+                continue
+            string = self._createLabelFromContents(prevObj)
+            if string:
+                return string
+
+        return None
+
+    def inferFromTextBelow(self, obj, proximity=20):
+        """Attempt to infer the functional/displayed label of obj by
+        looking at the contents of the line above the line containing
+        the object obj.
+
+        Arguments
+        - obj: the unlabeled widget
+        - proximity: pixels expected for a match
+
+        Returns the text which we think is the label, or None.
+        """
+
+        thisLine = self._getLineContents(obj)
+        nextObj, start, end, string = thisLine[-1]
+        if obj == nextObj:
+            start, end = self._script.utilities.getHyperlinkRange(nextObj)
+            nextObj = nextObj.parent
+
+        try:
+            text = nextObj.queryText()
+        except (AttributeError, NotImplementedError):
+            return None
+
+        objX, objY, objWidth, objHeight = self._getExtents(obj)
+        if not (objWidth and objHeight):
+            return None
+
+        boundary = pyatspi.TEXT_BOUNDARY_LINE_START
+        line = text.getTextAfterOffset(end - 1, boundary)
+        string = line[0].strip()
+        if string:
+            x, y, width, height = self._getExtents(nextObj, start, end)
+            distance = y - (objY + objHeight)
+            if distance <= proximity:
+                return string
 
         return None
 
@@ -280,7 +471,7 @@ class LabelInference:
             if label:
                 return label
 
-        if row < table.nRows:
+        if row < table.nRows and not self._preferTop(obj):
             candidate = table.getAccessibleAt(row + 1, col)
             label = self._createLabelFromContents(candidate)
             if label:
@@ -294,6 +485,9 @@ class LabelInference:
             return None
 
         cells = [table.getAccessibleAt(i, col) for i in range(1, table.nRows)]
+        if filter(lambda x: x == None, cells):
+            debug.println(debug.LEVEL_FINE, "INFER: Potentially broken table!")
+            return None
         if filter(lambda x: x[0] and x[0].getRole() != obj.getRole(), cells):
             return None
 
@@ -302,15 +496,3 @@ class LabelInference:
             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/script_utilities.py b/src/orca/script_utilities.py
index bde8292..4cc2c7e 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -930,9 +930,14 @@ class Utilities:
         # more than once.  Go figure, but we need to check for this.
         #
         label = []
-        relations = obj.getRelationSet()
-        allTargets = []
+        try:
+            relations = obj.getRelationSet()
+        except (LookupError, RuntimeError):
+            debug.println(debug.LEVEL_SEVERE,
+                          "labelsForObject() - Error getting RelationSet")
+            return label
 
+        allTargets = []
         for relation in relations:
             if relation.getRelationType() \
                    == pyatspi.RELATION_LABELLED_BY:



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