[orca] Web: Improve presentation of articles in feeds



commit 51acbd462c17d54b31926bf3339d96b6f141d62c
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Tue Jun 21 17:31:35 2022 +0200

    Web: Improve presentation of articles in feeds
    
    * Announce setsize when entering the feed
    * Support posinset and setsize on feed articles
    * Eliminate some chattiness (double-presentation of article name)
    * Present the feed article rather than the current line when
      page-up/page-down is used. We do this because normally these
      keystrokes are consumed by the browser, but the ARIA authoring
      practices example suggests using those to navigate among feed
      articles even in browse mode.

 src/orca/formatting.py                   | 11 +++++++++++
 src/orca/generator.py                    |  4 ++++
 src/orca/messages.py                     | 13 +++++++++++++
 src/orca/script_utilities.py             |  3 +++
 src/orca/scripts/web/script.py           |  4 +++-
 src/orca/scripts/web/script_utilities.py |  9 +++++++++
 src/orca/scripts/web/speech_generator.py | 13 +++++++++++--
 src/orca/speech_generator.py             | 10 +++++++---
 8 files changed, 61 insertions(+), 6 deletions(-)
---
diff --git a/src/orca/formatting.py b/src/orca/formatting.py
index 7981cb2bc..7e0ff7cff 100644
--- a/src/orca/formatting.py
+++ b/src/orca/formatting.py
@@ -139,6 +139,9 @@ formatting = {
             'focused': 'labelOrName + roleName',
             'unfocused': 'labelOrName + roleName + pause + currentLineText + allTextSelection',
             },
+        'ROLE_ARTICLE_IN_FEED' : {
+            'unfocused': '(labelOrName or currentLineText or roleName) + pause + positionInList',
+            },
         pyatspi.ROLE_BLOCK_QUOTE: {
             'focused' : 'leaving or (roleName + pause + nestingLevel)',
             'unfocused': 'roleName + pause + nestingLevel + pause + displayedText',
@@ -248,6 +251,10 @@ formatting = {
             'basicWhereAmI': 'labelOrName + readOnly + textRole + (textContent or placeholderText) + 
anyTextSelection + required + pause + invalid + ' + MNEMONIC,
             'detailedWhereAmI': 'labelOrName + readOnly + textRole + (textContentWithAttributes or 
placeholderText) + anyTextSelection + required + pause + invalid + ' + MNEMONIC,
             },
+        'ROLE_FEED': {
+            'focused': 'leaving or (labelOrName + pause + (numberOfChildren or roleName))',
+            'unfocused': 'labelOrName + pause + (numberOfChildren or roleName)',
+            },
         pyatspi.ROLE_FOOTNOTE: {
             'unfocused': 'labelOrName + roleName + pause + currentLineText + allTextSelection',
             },
@@ -605,6 +612,10 @@ formatting = {
                           or ([Component(obj, asString(labelAndName + roleName))]\
                              + (childWidget and ([Region(" ")] + childWidget))))'
             },
+        'ROLE_ARTICLE_IN_FEED': {
+            'unfocused': '((substring and ' + BRAILLE_TEXT + ')\
+                          or ([Component(obj, asString(labelOrName + roleName))]))'
+            },
         #pyatspi.ROLE_ARROW: 'default'
         pyatspi.ROLE_BLOCK_QUOTE: {
             'unfocused': BRAILLE_TEXT + ' + (roleName and [Region(" " + asString(roleName + 
nestingLevel))])',
diff --git a/src/orca/generator.py b/src/orca/generator.py
index 11161b3f3..8289885ac 100644
--- a/src/orca/generator.py
+++ b/src/orca/generator.py
@@ -1342,6 +1342,10 @@ class Generator:
             return pyatspi.ROLE_DESCRIPTION_TERM
         if self._script.utilities.isDescriptionListDescription(obj):
             return pyatspi.ROLE_DESCRIPTION_VALUE
+        if self._script.utilities.isFeedArticle(obj):
+            return 'ROLE_ARTICLE_IN_FEED'
+        if self._script.utilities.isFeed(obj):
+            return 'ROLE_FEED'
         if self._script.utilities.isLandmark(obj):
             if self._script.utilities.isLandmarkRegion(obj):
                 return 'ROLE_REGION'
diff --git a/src/orca/messages.py b/src/orca/messages.py
index 4c5886653..48bb75d72 100644
--- a/src/orca/messages.py
+++ b/src/orca/messages.py
@@ -2571,6 +2571,19 @@ def listItemCount(count):
     # Translators: This message describes a bulleted or numbered list.
     return ngettext("List with %d item", "List with %d items", count) % count
 
+def feedArticleCount(count):
+    if count == -1:
+        # Translators: This message describes a news/article feed whose size is
+        # unknown, such as can be found on social media sites that have unlimited
+        # scrolling, adding and/or removing items as the user moves up or down.
+        # Normally Orca announces "feed with n articles" when the count is known.
+        # This is the corresponding message for the unknown-count scenario.
+        return _("Feed of unknown size")
+
+    # Translators: This message describes the number of articles (news items,
+    # social media posts, etc.) in a feed.
+    return ngettext("Feed with %d article", "Feed with %d articles", count) % count
+
 def descriptionListTermCount(count):
     # Translators: This message describes a description list.
     # See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dl
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index 1d51fa490..496d633bb 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -950,6 +950,9 @@ class Utilities:
     def isFeed(self, obj):
         return False
 
+    def isFeedArticle(self, obj):
+        return False
+
     def isFigure(self, obj):
         return False
 
diff --git a/src/orca/scripts/web/script.py b/src/orca/scripts/web/script.py
index 3714c21c9..ad24f1e08 100644
--- a/src/orca/scripts/web/script.py
+++ b/src/orca/scripts/web/script.py
@@ -1349,7 +1349,9 @@ class Script(default.Script):
             msg = "WEB: New focus %s is anchor. Generating line contents." % newFocus
             debug.println(debug.LEVEL_INFO, msg, True)
             contents = self.utilities.getLineContentsAtOffset(newFocus, 0)
-        elif self.utilities.lastInputEventWasPageNav() and not self.utilities.getTable(newFocus):
+        elif self.utilities.lastInputEventWasPageNav() \
+             and not self.utilities.getTable(newFocus) \
+             and not self.utilities.isFeedArticle(newFocus):
             msg = "WEB: New focus %s was scrolled to. Generating line contents." % newFocus
             debug.println(debug.LEVEL_INFO, msg, True)
             contents = self.utilities.getLineContentsAtOffset(newFocus, caretOffset)
diff --git a/src/orca/scripts/web/script_utilities.py b/src/orca/scripts/web/script_utilities.py
index 1ad2964f6..10fb35e41 100644
--- a/src/orca/scripts/web/script_utilities.py
+++ b/src/orca/scripts/web/script_utilities.py
@@ -3931,6 +3931,15 @@ class Utilities(script_utilities.Utilities):
     def isFeed(self, obj):
         return 'feed' in self._getXMLRoles(obj)
 
+    def isFeedArticle(self, obj):
+        if not (obj and self.inDocumentContent(obj)):
+            return False
+
+        if obj.getRole() != pyatspi.ROLE_ARTICLE:
+            return False
+
+        return pyatspi.findAncestor(obj, self.isFeed) is not None
+
     def isFigure(self, obj):
         return 'figure' in self._getXMLRoles(obj) or self._getTag(obj) == 'figure'
 
diff --git a/src/orca/scripts/web/speech_generator.py b/src/orca/scripts/web/speech_generator.py
index 0cd04abdf..7dd12b93f 100644
--- a/src/orca/scripts/web/speech_generator.py
+++ b/src/orca/scripts/web/speech_generator.py
@@ -157,6 +157,9 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
         if not self._script.utilities.inDocumentContent(obj):
             return []
 
+        if self._script.utilities.isFeedArticle(obj):
+            return []
+
         if not args.get('mode', None):
             args['mode'] = self._mode
 
@@ -450,15 +453,19 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
         # We handle things even for non-document content due to issues in
         # other toolkits (e.g. exposing list items to us that are not
         # exposed to sighted users)
+        roles = [pyatspi.ROLE_DESCRIPTION_LIST,
+                 pyatspi.ROLE_LIST,
+                 pyatspi.ROLE_LIST_BOX,
+                 'ROLE_FEED']
         role = args.get('role', obj.getRole())
-        if role not in [pyatspi.ROLE_LIST, pyatspi.ROLE_LIST_BOX, pyatspi.ROLE_DESCRIPTION_LIST]:
+        if role not in roles:
             return super()._generateNumberOfChildren(obj, **args)
 
         setsize = self._script.utilities.getSetSize(obj[0])
         if setsize is None:
             if self._script.utilities.isDescriptionList(obj):
                 children = [x for x in obj if self._script.utilities.isDescriptionListTerm(x)]
-            else:
+            elif role in [pyatspi.ROLE_LIST, pyatspi.ROLE_LIST_BOX]:
                 children = [x for x in obj if x.getRole() == pyatspi.ROLE_LIST_ITEM]
             setsize = len(children)
 
@@ -467,6 +474,8 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
 
         if self._script.utilities.isDescriptionList(obj):
             result = [messages.descriptionListTermCount(setsize)]
+        elif role == 'ROLE_FEED':
+            result = [messages.feedArticleCount(setsize)]
         else:
             result = [messages.listItemCount(setsize)]
         result.extend(self.voice(speech_generator.SYSTEM, obj=obj, **args))
diff --git a/src/orca/speech_generator.py b/src/orca/speech_generator.py
index 40cc0ab48..2cb0990ca 100644
--- a/src/orca/speech_generator.py
+++ b/src/orca/speech_generator.py
@@ -1830,6 +1830,7 @@ class SpeechGenerator(generator.Generator):
                     'ROLE_DPUB_LANDMARK',
                     'ROLE_DPUB_SECTION',
                     pyatspi.ROLE_DESCRIPTION_LIST,
+                    'ROLE_FEED',
                     pyatspi.ROLE_FORM,
                     pyatspi.ROLE_LANDMARK,
                     pyatspi.ROLE_LIST,
@@ -1847,6 +1848,7 @@ class SpeechGenerator(generator.Generator):
             if _settingsManager.getSetting('sayAllContextList'):
                 enabled.append(pyatspi.ROLE_LIST)
                 enabled.append(pyatspi.ROLE_DESCRIPTION_LIST)
+                enabled.append('ROLE_FEED')
             if _settingsManager.getSetting('sayAllContextPanel'):
                 enabled.extend([pyatspi.ROLE_PANEL,
                                 pyatspi.ROLE_TOOL_TIP,
@@ -1867,6 +1869,7 @@ class SpeechGenerator(generator.Generator):
             if _settingsManager.getSetting('speakContextList'):
                 enabled.append(pyatspi.ROLE_LIST)
                 enabled.append(pyatspi.ROLE_DESCRIPTION_LIST)
+                enabled.append('ROLE_FEED')
             if _settingsManager.getSetting('speakContextPanel'):
                 enabled.extend([pyatspi.ROLE_PANEL,
                                 pyatspi.ROLE_TOOL_TIP,
@@ -1908,10 +1911,10 @@ class SpeechGenerator(generator.Generator):
                 result.append(messages.leavingNLists(count))
             else:
                 result.append(messages.LEAVING_LIST)
+        elif role == 'ROLE_FEED':
+            result.append(messages.LEAVING_FEED)
         elif role == pyatspi.ROLE_PANEL:
-            if self._script.utilities.isFeed(obj):
-                result.append(messages.LEAVING_FEED)
-            elif self._script.utilities.isFigure(obj):
+            if self._script.utilities.isFigure(obj):
                 result.append(messages.LEAVING_FIGURE)
             elif self._script.utilities.isDocumentPanel(obj):
                 result.append(messages.LEAVING_PANEL)
@@ -2153,6 +2156,7 @@ class SpeechGenerator(generator.Generator):
                                'ROLE_CONTENT_SUGGESTION',
                                'ROLE_DPUB_LANDMARK',
                                'ROLE_DPUB_SECTION',
+                               'ROLE_FEED',
                                pyatspi.ROLE_LIST,
                                pyatspi.ROLE_PANEL,
                                'ROLE_REGION',


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