[orca] Begin work on label inference (aka "label guess") functionality for WebKitGtk content



commit ec2cbd70501101e6ceaa8fde1646ee55b9ec7b31
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Sun Dec 4 20:56:39 2011 +0100

    Begin work on label inference (aka "label guess") functionality for WebKitGtk content

 src/orca/Makefile.am                               |    1 +
 src/orca/label_inference.py                        |  198 ++++++++++++++++++++
 src/orca/script.py                                 |    6 +
 src/orca/script_utilities.py                       |   30 +++-
 .../scripts/toolkits/WebKitGtk/script_utilities.py |   12 --
 .../scripts/toolkits/WebKitGtk/speech_generator.py |   29 +++-
 6 files changed, 261 insertions(+), 15 deletions(-)
---
diff --git a/src/orca/Makefile.am b/src/orca/Makefile.am
index 30288eb..fab8a97 100644
--- a/src/orca/Makefile.am
+++ b/src/orca/Makefile.am
@@ -34,6 +34,7 @@ orca_python_PYTHON = \
 	input_event.py \
 	keybindings.py \
 	keynames.py \
+	label_inference.py \
 	laptop_keyboardmap.py \
 	liveregions.py \
 	mouse_review.py \
diff --git a/src/orca/label_inference.py b/src/orca/label_inference.py
new file mode 100644
index 0000000..7cd0c13
--- /dev/null
+++ b/src/orca/label_inference.py
@@ -0,0 +1,198 @@
+# Orca
+#
+# Copyright (C) 2011 Igalia, S.L.
+#
+# Author: Joanmarie Diggs <jdiggs igalia com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+"""Heuristic means to infer the functional/displayed label of a widget."""
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (C) 2011 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+
+class LabelInference:
+
+    def __init__(self, script):
+        """Creates an instance of the LabelInference class.
+
+        Arguments:
+        - script: the script with which this instance is associated.
+        """
+
+        self._script = script
+
+    def infer(self, obj, focusedOnly=True):
+        """Attempt to infer the functional/displayed label of obj.
+
+        Arguments
+        - obj: the unlabeled widget
+        - focusedOnly: If True, only infer if the widget has focus.
+
+        Returns the text which we think is the label, or None.
+        """
+
+        isFocused = obj.getState().contains(pyatspi.STATE_FOCUSED)
+        if focusedOnly and not isFocused:
+            return None
+
+        result = None
+        if not result:
+            result = self.inferFromLine(obj)
+        if not result:
+            result = self.inferFromTable(obj)
+        if not result:
+            result = self.inferFromOtherLines(obj)
+        if not result:
+            result = obj.name
+        if not result:
+            result = obj.description
+        if result:
+            result = result.strip()
+
+        return result
+
+    def _isWidget(self, obj):
+        """Returns True if the given object is a widget."""
+
+        widgetRoles = [pyatspi.ROLE_CHECK_BOX,
+                       pyatspi.ROLE_RADIO_BUTTON,
+                       pyatspi.ROLE_COMBO_BOX,
+                       pyatspi.ROLE_DOCUMENT_FRAME,
+                       pyatspi.ROLE_LIST,
+                       pyatspi.ROLE_ENTRY,
+                       pyatspi.ROLE_PASSWORD_TEXT,
+                       pyatspi.ROLE_PUSH_BUTTON]
+
+        return obj.getRole() in widgetRoles
+
+    def _getExtents(self, obj, startOffset, endOffset):
+        """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
+
+        try:
+            text = obj.queryText()
+        except AttributeError:
+            return extents
+        except NotImplementedError:
+            pass
+        else:
+            skipTextExtents = [pyatspi.ROLE_ENTRY, pyatspi.ROLE_PASSWORD_TEXT]
+            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
+
+        return extents
+
+    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:
+            start, end = self._script.utilities.getHyperlinkRange(obj)
+            contents = self._script.utilities.getObjectsFromEOCs(
+                obj.parent, boundary, start)
+
+        return contents
+
+    def inferFromLine(self, obj, proximity=75):
+        """Attempt to infer the functional/displayed label of obj by
+        looking at the contents of the current line.
+
+        Arguments
+        - obj: the unlabeled widget
+        - proximity: pixels expected for a match
+
+        Returns the text which we think is the label, or None.
+        """
+
+        contents = self._getLineContents(obj)
+        content = filter(lambda o: o[0] == obj, contents)
+        try:
+            index = contents.index(content[0])
+        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 = 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
+            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:
+                    return lString
+
+        if onRight:
+            rObj, rStart, rEnd, rString = onRight
+            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:
+                    return rString
+
+        if rString and rDistance <= proximity:
+            return rString
+
+        return None
+
+    def inferFromTable(self, obj):
+        pass
+
+    def inferFromOtherLines(self, obj):
+        pass
diff --git a/src/orca/script.py b/src/orca/script.py
index e8a86a7..61adfab 100644
--- a/src/orca/script.py
+++ b/src/orca/script.py
@@ -45,6 +45,7 @@ import braille_generator
 import debug
 import flat_review
 import formatting
+import label_inference
 import keybindings
 import orca_state
 import script_utilities
@@ -83,6 +84,7 @@ class Script:
         self.presentIfInactive = True
 
         self.utilities = self.getUtilities()
+        self.labelInference = self.getLabelInference()
         self.structuralNavigation = self.getStructuralNavigation()
         self.chat = self.getChat()
         self.inputEventHandlers = {}
@@ -211,6 +213,10 @@ class Script:
         """
         return script_utilities.Utilities(self)
 
+    def getLabelInference(self):
+        """Returns the label inference functionality for this script."""
+        return label_inference.LabelInference(self)
+
     def getEnabledStructuralNavigationTypes(self):
         """Returns a list of the structural navigation object types
         enabled in this script.
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index 90672e4..dea3136 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -2452,7 +2452,7 @@ class Utilities:
 
         try:
             unchanged = newSegment == segment
-        except UnicodeEncodeError, UnicodeDecodeError:
+        except (UnicodeEncodeError, UnicodeDecodeError):
             unchanged = True
 
         if unchanged:
@@ -2965,3 +2965,31 @@ class Utilities:
         except:
             debug.printException(debug.LEVEL_WARNING)
             return ""
+
+    def getObjectsFromEOCs(self, obj, boundary=None, offset=None):
+        """Breaks the string containing a mixture of text and embedded object
+        characters into a list of (obj, startOffset, endOffset, string) tuples.
+
+        Arguments
+        - obj: the object whose EOCs we need to expand into tuples
+        - boundary: the pyatspi text boundary type. If None, get all text.
+        - offset: the character offset. If None, use the current offset.
+
+        Returns a list of (obj, startOffset, endOffset, string) tuples.
+        """
+
+        # For now, each script should implement this functionality itself.
+
+        return []
+
+    @staticmethod
+    def getHyperlinkRange(obj):
+        """Returns the start and end indices associated with the embedded
+        object, obj."""
+
+        try:
+            hyperlink = obj.queryHyperlink()
+        except NotImplementedError:
+            return 0, 0
+
+        return hyperlink.startIndex, hyperlink.endIndex
diff --git a/src/orca/scripts/toolkits/WebKitGtk/script_utilities.py b/src/orca/scripts/toolkits/WebKitGtk/script_utilities.py
index a25109e..4a1624d 100644
--- a/src/orca/scripts/toolkits/WebKitGtk/script_utilities.py
+++ b/src/orca/scripts/toolkits/WebKitGtk/script_utilities.py
@@ -112,18 +112,6 @@ class Utilities(script_utilities.Utilities):
 
         return text
 
-    @staticmethod
-    def getHyperlinkRange(obj):
-        """Returns the start and end indices associated with the embedded
-        object, obj."""
-
-        try:
-            hyperlink = obj.queryHyperlink()
-        except NotImplementedError:
-            return 0, 0
-
-        return hyperlink.startIndex, hyperlink.endIndex
-
     def getObjectsFromEOCs(self, obj, boundary=None, offset=None):
         """Breaks the string containing a mixture of text and embedded object
         characters into a list of (obj, startOffset, endOffset, string) tuples.
diff --git a/src/orca/scripts/toolkits/WebKitGtk/speech_generator.py b/src/orca/scripts/toolkits/WebKitGtk/speech_generator.py
index 5be3def..923c8fe 100644
--- a/src/orca/scripts/toolkits/WebKitGtk/speech_generator.py
+++ b/src/orca/scripts/toolkits/WebKitGtk/speech_generator.py
@@ -1,8 +1,9 @@
 # Orca
 #
 # Copyright (C) 2010 Joanmarie Diggs
+# Copyright (C) 2011 Igalia, S.L.
 #
-# Author: Joanmarie Diggs <joanied gnome org>
+# Author: Joanmarie Diggs <jdiggs igalia com>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
@@ -22,7 +23,8 @@
 __id__        = "$Id$"
 __version__   = "$Revision$"
 __date__      = "$Date$"
-__copyright__ = "Copyright (c) 2010 Joanmarie Diggs"
+__copyright__ = "Copyright (c) 2010 Joanmarie Diggs" \
+                "Copyright (c) 2011 Igalia, S.L."
 __license__   = "LGPL"
 
 import pyatspi
@@ -56,6 +58,29 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
 
         return voice
 
+    def _generateLabel(self, obj, **args):
+        result = \
+            speech_generator.SpeechGenerator._generateLabel(self, obj, **args)
+        if result:
+            return result
+
+        role = args.get('role', obj.getRole())
+        inferRoles = [pyatspi.ROLE_CHECK_BOX,
+                      pyatspi.ROLE_COMBO_BOX,
+                      pyatspi.ROLE_ENTRY,
+                      pyatspi.ROLE_LIST,
+                      pyatspi.ROLE_PASSWORD_TEXT,
+                      pyatspi.ROLE_RADIO_BUTTON]
+        if not role in inferRoles:
+            return result
+
+        label = self._script.labelInference.infer(obj)
+        if label:
+            result.append(label)
+            result.extend(self.voice(speech_generator.DEFAULT))
+
+        return result
+
     def _generateRoleName(self, obj, **args):
         if _settingsManager.getSetting('onlySpeakDisplayedText'):
             return []



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