[orca] Continued work on support for aria-details



commit 8dcd93c3f6ae8066ab47654cc6c235fb66054bc4
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Tue Jan 7 12:43:21 2020 +0100

    Continued work on support for aria-details
    
    * Announce when entering and exiting details container via caret
      navigation. When entering, announce the name and type of object
      the details apply to.
    * Announce the name and type of object when an object has details.

 src/orca/formatting.py                   |  7 ++--
 src/orca/generator.py                    |  3 ++
 src/orca/messages.py                     | 11 ++++++
 src/orca/object_properties.py            | 36 ++++++++++++++----
 src/orca/script_utilities.py             |  6 +++
 src/orca/scripts/web/script_utilities.py | 65 ++++++++++++++++++++++++++++++++
 src/orca/scripts/web/speech_generator.py | 36 +++++++++++++++---
 src/orca/speech_generator.py             | 16 +++++++-
 8 files changed, 163 insertions(+), 17 deletions(-)
---
diff --git a/src/orca/formatting.py b/src/orca/formatting.py
index f213b8eac..61955d26f 100644
--- a/src/orca/formatting.py
+++ b/src/orca/formatting.py
@@ -69,7 +69,8 @@ formatting = {
             'groupindex': object_properties.GROUP_INDEX_SPEECH,
             'clickable': object_properties.STATE_CLICKABLE,
             'haslongdesc': object_properties.STATE_HAS_LONGDESC,
-            'hasdetails': object_properties.STATE_HAS_DETAILS,
+            'hasdetails': object_properties.RELATION_HAS_DETAILS,
+            'detailsfor': object_properties.RELATION_DETAILS_FOR
         },
         'braille': {
             'eol': object_properties.EOL_INDICATOR_BRAILLE,
@@ -110,14 +111,14 @@ formatting = {
 
     'speech': {
         'prefix': {
-            'focused': '[]',
+            'focused': 'detailsFor',
             'unfocused': 'oldAncestors + newAncestors',
             'basicWhereAmI': 'toolbar',
             'detailedWhereAmI' : '[]'
             },
         'suffix': {
             'focused': '[]',
-            'unfocused': 'newNodeLevel + unselectedCell + clickable + pause + hasLongDesc + hasDetails +' + 
TUTORIAL + ' + description + pause + hasPopup',
+            'unfocused': 'newNodeLevel + unselectedCell + clickable + pause + hasLongDesc + hasDetails + 
detailsFor +' + TUTORIAL + ' + description + pause + hasPopup',
             'basicWhereAmI': TUTORIAL + ' + clickable + hasLongDesc + description + pause + hasPopup',
             'detailedWhereAmI': TUTORIAL + ' + clickable + hasLongDesc + description + pause + hasPopup'
             },
diff --git a/src/orca/generator.py b/src/orca/generator.py
index d072320ec..328a8f604 100644
--- a/src/orca/generator.py
+++ b/src/orca/generator.py
@@ -489,6 +489,9 @@ class Generator:
     def _generateHasDetails(self, obj, **args):
         return []
 
+    def _generateDetailsFor(self, obj, **args):
+        return []
+
     def _generateHasPopup(self, obj, **args):
         return []
 
diff --git a/src/orca/messages.py b/src/orca/messages.py
index 9e5725f75..dbe8811bf 100644
--- a/src/orca/messages.py
+++ b/src/orca/messages.py
@@ -1112,6 +1112,17 @@ LEARN_MODE_START_SPEECH = \
 # blockquote and then navigates out of it.
 LEAVING_BLOCKQUOTE = _("leaving blockquote.")
 
+# Translators: In web content, authors can identify an element which contains
+# detailed information about another element. For instance, for a password
+# field, there may be a list of requirements (number of characters, number of
+# special symbols, etc.). For an image, there may be an extended description
+# before or after the image. Often there are visual clues connecting the
+# detailed information to its related object. We need to convey this non-visually.
+# This message is presented when a user just navigated out of a container holding
+# detailed information about another object.
+# See https://w3c.github.io/aria/#aria-details
+LEAVING_DETAILS = _("leaving details.")
+
 # Translators: This message is presented when a user is navigating within
 # an object and then navigates out of it. The word or phrase that follows
 # "leaving" should be consistent with the translation provided for the
diff --git a/src/orca/object_properties.py b/src/orca/object_properties.py
index 8d6bf65b6..a64891427 100644
--- a/src/orca/object_properties.py
+++ b/src/orca/object_properties.py
@@ -65,6 +65,35 @@ NODE_LEVEL_SPEECH = _("tree level %d")
 # ancestors the node has). This is the braille version.
 NODE_LEVEL_BRAILLE = _("TREE LEVEL %d")
 
+# Translators: In web content, authors can identify an element which contains
+# detailed information about another element. For instance, for a password
+# field, there may be a list of requirements (number of characters, number of
+# special symbols, etc.). For an image, there may be an extended description
+# before or after the image. Often there are visual clues connecting the
+# detailed information to its related object. We need to convey this non-visually.
+# This relationship will be presented for the object containing the details, e.g.
+# when arrowing into or out of it. The string substitution is for the object to
+# which the detailed information applies. For instance, when navigating into
+# the details for an image named Pythagorean Theorem, Orca would present:
+# "details for Pythagorean Theorem image".
+# See https://w3c.github.io/aria/#aria-details
+RELATION_DETAILS_FOR = _("details for %s")
+
+# Translators: In web content, authors can identify an element which contains
+# detailed information about another element. For instance, for a password
+# field, there may be a list of requirements (number of characters, number of
+# special symbols, etc.). For an image, there may be an extended description
+# before or after the image. Often there are visual clues connecting the
+# detailed information to its related object. We need to convey this non-visually.
+# This relationship will be presented for the object which has details to tell
+# the user the type of object where the details can be found so that they can
+# more quickly navigate to it. The string substitution is for the object to
+# which the detailed information applies. For instance, when navigating to
+# a password field which has details in a list named "Requirements", Orca would
+# present: "has details in Requirements list".
+# See https://w3c.github.io/aria/#aria-details
+RELATION_HAS_DETAILS = _("has details in %s")
+
 # Translators: This string should be treated as a role describing an object.
 # Examples of roles include "checkbox", "radio button", "paragraph", and "link."
 # The reason for including the editable state as part of the role is to make it
@@ -453,13 +482,6 @@ STATE_EXPANDED = _("expanded")
 # which have a longdesc attribute. http://www.w3.org/TR/WCAG20-TECHS/H45.html
 STATE_HAS_LONGDESC = _("has long description")
 
-# Translators: This is a state which applies to elements in document content
-# which have a detailed description or explanation. That description might
-# be hidden or might be in a different location on the page. Therefore Orca
-# announces the presence of the additional information so that the user can
-# use native application and/or Orca commands to read those details.
-STATE_HAS_DETAILS = _("has details")
-
 # Translators: This is a state which applies to the orientation of widgets
 # such as sliders and scroll bars.
 STATE_HORIZONTAL = _("horizontal")
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index 0134513fc..fd95e3550 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -3943,6 +3943,12 @@ class Utilities:
     def hasDetails(self, obj):
         return False
 
+    def isDetails(self, obj):
+        return False
+
+    def detailsFor(self, obj):
+        return []
+
     def popupType(self, obj):
         return ''
 
diff --git a/src/orca/scripts/web/script_utilities.py b/src/orca/scripts/web/script_utilities.py
index 272ebb79a..f7f6afcc8 100644
--- a/src/orca/scripts/web/script_utilities.py
+++ b/src/orca/scripts/web/script_utilities.py
@@ -77,6 +77,7 @@ class Utilities(script_utilities.Utilities):
         self._hasNoSize = {}
         self._hasLongDesc = {}
         self._hasDetails = {}
+        self._isDetails = {}
         self._popupType = {}
         self._hasUselessCanvasDescendant = {}
         self._id = {}
@@ -158,6 +159,7 @@ class Utilities(script_utilities.Utilities):
         self._hasNoSize = {}
         self._hasLongDesc = {}
         self._hasDetails = {}
+        self._isDetails = {}
         self._popupType = {}
         self._hasUselessCanvasDescendant = {}
         self._id = {}
@@ -3604,6 +3606,69 @@ class Utilities(script_utilities.Utilities):
         self._hasDetails[hash(obj)] = rv
         return rv
 
+    def detailsIn(self, obj):
+        if not self.hasDetails(obj):
+            return []
+
+        try:
+            relations = obj.getRelationSet()
+        except:
+            msg = 'ERROR: Exception getting relationset for %s' % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return []
+
+        rv = []
+        relation = filter(lambda x: x.getRelationType() == pyatspi.RELATION_DETAILS, relations)
+        for r in relation:
+            for i in range(r.getNTargets()):
+                rv.append(r.getTarget(i))
+
+        return rv
+
+    def isDetails(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return super().isDetails(obj)
+
+        rv = self._isDetails.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        try:
+            relations = obj.getRelationSet()
+        except:
+            msg = 'ERROR: Exception getting relationset for %s' % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return False
+
+        rv = False
+        relation = filter(lambda x: x.getRelationType() == pyatspi.RELATION_DETAILS_FOR, relations)
+        for r in relation:
+            if r.getNTargets() > 0:
+                rv = True
+                break
+
+        self._isDetails[hash(obj)] = rv
+        return rv
+
+    def detailsFor(self, obj):
+        if not self.isDetails(obj):
+            return []
+
+        try:
+            relations = obj.getRelationSet()
+        except:
+            msg = 'ERROR: Exception getting relationset for %s' % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return []
+
+        rv = []
+        relation = filter(lambda x: x.getRelationType() == pyatspi.RELATION_DETAILS_FOR, relations)
+        for r in relation:
+            for i in range(r.getNTargets()):
+                rv.append(r.getTarget(i))
+
+        return rv
+
     def popupType(self, obj):
         if not (obj and self.inDocumentContent(obj)):
             return 'false'
diff --git a/src/orca/scripts/web/speech_generator.py b/src/orca/scripts/web/speech_generator.py
index 85f43b6d6..fd05dd6c7 100644
--- a/src/orca/scripts/web/speech_generator.py
+++ b/src/orca/scripts/web/speech_generator.py
@@ -205,13 +205,39 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
         if not self._script.utilities.inDocumentContent(obj):
             return super()._generateHasDetails(obj, **args)
 
+        objs = self._script.utilities.detailsIn(obj)
+        if not objs:
+            return []
+
+        objString = lambda x: "%s %s" % (x.name, self.getLocalizedRoleName(x))
+        toPresent = ", ".join(list(map(objString, objs)))
+
         args['stringType'] = 'hasdetails'
-        if self._script.utilities.hasDetails(obj):
-            result = [self._script.formatting.getString(**args)]
-            result.extend(self.voice(speech_generator.SYSTEM))
-            return result
+        result = [self._script.formatting.getString(**args) % toPresent]
+        result.extend(self.voice(speech_generator.SYSTEM))
+        return result
 
-        return []
+    def _generateDetailsFor(self, obj, **args):
+        if _settingsManager.getSetting('onlySpeakDisplayedText'):
+            return []
+
+        if not self._script.utilities.inDocumentContent(obj):
+            return super()._generateDetailsFor(obj, **args)
+
+        objs = self._script.utilities.detailsFor(obj)
+        if not objs:
+            return []
+
+        if args.get('leaving'):
+            return []
+
+        objString = lambda x: "%s %s" % (x.name, self.getLocalizedRoleName(x))
+        toPresent = ", ".join(list(map(objString, objs)))
+
+        args['stringType'] = 'detailsfor'
+        result = [self._script.formatting.getString(**args) % toPresent]
+        result.extend(self.voice(speech_generator.SYSTEM))
+        return result
 
     def _generateLabelOrName(self, obj, **args):
         if not self._script.utilities.inDocumentContent(obj):
diff --git a/src/orca/speech_generator.py b/src/orca/speech_generator.py
index 96234bad9..27bae30c6 100644
--- a/src/orca/speech_generator.py
+++ b/src/orca/speech_generator.py
@@ -296,6 +296,16 @@ class SpeechGenerator(generator.Generator):
             result.extend(acss)
         return result
 
+    def _generateDetailsFor(self, obj, **args):
+        if _settingsManager.getSetting('onlySpeakDisplayedText'):
+            return []
+
+        acss = self.voice(SYSTEM)
+        result = generator.Generator._generateDetailsFor(self, obj, **args)
+        if result:
+            result.extend(acss)
+        return result
+
     def _generateAvailability(self, obj, **args):
         if _settingsManager.getSetting('onlySpeakDisplayedText'):
             return []
@@ -1632,13 +1642,15 @@ class SpeechGenerator(generator.Generator):
 
         role = args.get('role', obj.getRole())
         enabled, disabled = self._getEnabledAndDisabledContextRoles()
-        if not role in enabled:
+        if not (role in enabled or self._script.utilities.isDetails(obj)):
             return []
 
         count = args.get('count', 1)
 
         result = []
-        if role == pyatspi.ROLE_BLOCK_QUOTE:
+        if self._script.utilities.isDetails(obj):
+            result.append(messages.LEAVING_DETAILS)
+        elif role == pyatspi.ROLE_BLOCK_QUOTE:
             if count > 1:
                 result.append(messages.leavingNBlockquotes(count))
             else:


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