[orca] Support navigation within focusable tooltips in web applications



commit fd16615c4cacbb4ee389bd37457978af072c0fd3
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Fri Apr 17 15:31:55 2020 -0400

    Support navigation within focusable tooltips in web applications

 src/orca/formatting.py                   |  5 +++--
 src/orca/messages.py                     |  4 ++++
 src/orca/scripts/web/script.py           | 28 ++++++++++++++++++------
 src/orca/scripts/web/script_utilities.py | 37 ++++++++++++++++++++++++++++----
 src/orca/scripts/web/speech_generator.py |  3 ++-
 src/orca/speech_generator.py             | 19 +++++++++++-----
 6 files changed, 78 insertions(+), 18 deletions(-)
---
diff --git a/src/orca/formatting.py b/src/orca/formatting.py
index 9512e35cc..38ea48975 100644
--- a/src/orca/formatting.py
+++ b/src/orca/formatting.py
@@ -508,8 +508,9 @@ formatting = {
             'unfocused': 'labelAndName + roleName',
             },
         pyatspi.ROLE_TOOL_TIP: {
-            'unfocused': 'labelAndName',
-            'basicWhereAmI': 'labelAndName'
+            'focused': 'leaving or roleName',
+            'unfocused': 'roleName + labelAndName',
+            'basicWhereAmI': 'roleName + labelAndName'
             },
         pyatspi.ROLE_TREE_ITEM: {
             'focused': 'expandableState',
diff --git a/src/orca/messages.py b/src/orca/messages.py
index 58199759a..e314999d9 100644
--- a/src/orca/messages.py
+++ b/src/orca/messages.py
@@ -1231,6 +1231,10 @@ LEAVING_PANEL = _("leaving panel.")
 # table and then navigates out of it.
 LEAVING_TABLE = _("leaving table.")
 
+# Translators: This message is presented when a user is navigating within a
+# tooltip in a web application and then navigates out of it.
+LEAVING_TOOL_TIP = _("leaving tooltip.")
+
 # Translators: This message is presented when a user is navigating within
 # a document container and then navigates out of it. The word or phrase
 # that follows "leaving" should be consistent with the translation provided
diff --git a/src/orca/scripts/web/script.py b/src/orca/scripts/web/script.py
index 6fcb906cf..8866a3c51 100644
--- a/src/orca/scripts/web/script.py
+++ b/src/orca/scripts/web/script.py
@@ -778,7 +778,7 @@ class Script(default.Script):
 
         return self._browseModeIsSticky
 
-    def useFocusMode(self, obj):
+    def useFocusMode(self, obj, prevObj=None):
         """Returns True if we should use focus mode in obj."""
 
         if self._focusModeIsSticky:
@@ -798,7 +798,8 @@ class Script(default.Script):
             return False
 
         if not _settingsManager.getSetting('caretNavTriggersFocusMode') \
-           and self._lastCommandWasCaretNav:
+           and self._lastCommandWasCaretNav \
+           and not self.utilities.isNavigableToolTipDescendant(prevObj):
             msg = "WEB: Not using focus mode due to caret nav settings"
             debug.println(debug.LEVEL_INFO, msg, True)
             return False
@@ -816,6 +817,11 @@ class Script(default.Script):
             return True
 
         if self._inFocusMode and self.utilities.isWebAppDescendant(obj):
+            if self.utilities.forceBrowseModeForWebAppDescendant(obj):
+                msg = "WEB: Forcing browse mode for web app descendant %s" % obj
+                debug.println(debug.LEVEL_INFO, msg, True)
+                return False
+
             msg = "WEB: Staying in focus mode because we're inside a web application"
             debug.println(debug.LEVEL_INFO, msg, True)
             return True
@@ -1275,7 +1281,7 @@ class Script(default.Script):
 
         if not self._focusModeIsSticky \
            and not self._browseModeIsSticky \
-           and self.useFocusMode(newFocus) != self._inFocusMode:
+           and self.useFocusMode(newFocus, oldFocus) != self._inFocusMode:
             self.togglePresentationMode(None)
 
         return True
@@ -1840,10 +1846,16 @@ class Script(default.Script):
             debug.println(debug.LEVEL_INFO, msg, True)
             return False
 
+        role = event.source.getRole()
         if self.utilities.isWebAppDescendant(event.source):
             if self._browseModeIsSticky:
                 msg = "WEB: Web app descendant claimed focus, but browse mode is sticky"
                 debug.println(debug.LEVEL_INFO, msg, True)
+            elif role == pyatspi.ROLE_TOOL_TIP \
+                 and pyatspi.findAncestor(orca_state.locusOfFocus, lambda x: x and x == event.source):
+                msg = "WEB: Event believed to be side effect of tooltip navigation."
+                debug.println(debug.LEVEL_INFO, msg, True)
+                return True
             else:
                 msg = "WEB: Event handled: Setting locusOfFocus to web app descendant"
                 debug.println(debug.LEVEL_INFO, msg, True)
@@ -1856,7 +1868,6 @@ class Script(default.Script):
             debug.println(debug.LEVEL_INFO, msg, True)
             return False
 
-        role = event.source.getRole()
         if role in [pyatspi.ROLE_DIALOG, pyatspi.ROLE_ALERT]:
             msg = "WEB: Event handled: Setting locusOfFocus to event source"
             debug.println(debug.LEVEL_INFO, msg, True)
@@ -2021,9 +2032,14 @@ class Script(default.Script):
             return True
 
         if self.utilities.isWebAppDescendant(event.source):
-            msg = "WEB: Event source is web app descendant"
+            if self._inFocusMode:
+                msg = "WEB: Event source is web app descendant and we're in focus mode"
+                debug.println(debug.LEVEL_INFO, msg, True)
+                return False
+
+            msg = "WEB: Event source is web app descendant and we're in browse mode"
             debug.println(debug.LEVEL_INFO, msg, True)
-            return False
+            return True
 
         obj, offset = self.utilities.getCaretContext()
         ancestor = self.utilities.commonAncestor(obj, event.source)
diff --git a/src/orca/scripts/web/script_utilities.py b/src/orca/scripts/web/script_utilities.py
index 0b839b7f3..4d8b3d06c 100644
--- a/src/orca/scripts/web/script_utilities.py
+++ b/src/orca/scripts/web/script_utilities.py
@@ -66,6 +66,7 @@ class Utilities(script_utilities.Utilities):
         self._isGridDescendant = {}
         self._isLabelDescendant = {}
         self._isMenuDescendant = {}
+        self._isNavigableToolTipDescendant = {}
         self._isToolBarDescendant = {}
         self._isWebAppDescendant = {}
         self._isLayoutOnly = {}
@@ -139,6 +140,7 @@ class Utilities(script_utilities.Utilities):
         self._isGridDescendant = {}
         self._isLabelDescendant = {}
         self._isMenuDescendant = {}
+        self._isNavigableToolTipDescendant = {}
         self._isToolBarDescendant = {}
         self._isWebAppDescendant = {}
         self._isLayoutOnly = {}
@@ -393,7 +395,8 @@ class Utilities(script_utilities.Utilities):
         if self._script.focusModeIsSticky():
             return
 
-        self.clearTextSelection(orca_state.locusOfFocus)
+        oldFocus = orca_state.locusOfFocus
+        self.clearTextSelection(oldFocus)
         orca.setLocusOfFocus(None, obj, notifyScript=False)
         if grabFocus:
             self.grabFocus(obj)
@@ -409,7 +412,7 @@ class Utilities(script_utilities.Utilities):
                 msg = "WEB: Caret set to %i in %s" % (offset, obj)
                 debug.println(debug.LEVEL_INFO, msg, True)
 
-        if self._script.useFocusMode(obj) != self._script.inFocusMode():
+        if self._script.useFocusMode(obj, oldFocus) != self._script.inFocusMode():
             self._script.togglePresentationMode(None)
 
         if obj:
@@ -1007,7 +1010,6 @@ class Utilities(script_utilities.Utilities):
                  pyatspi.ROLE_PUSH_BUTTON,
                  pyatspi.ROLE_TOGGLE_BUTTON,
                  pyatspi.ROLE_TOOL_BAR,
-                 pyatspi.ROLE_TOOL_TIP,
                  pyatspi.ROLE_TREE,
                  pyatspi.ROLE_TREE_ITEM,
                  pyatspi.ROLE_TREE_TABLE]
@@ -1874,6 +1876,15 @@ class Utilities(script_utilities.Utilities):
 
         return False
 
+    def forceBrowseModeForWebAppDescendant(self, obj):
+        if not self.isWebAppDescendant(obj):
+            return False
+
+        if obj.getRole() == pyatspi.ROLE_TOOL_TIP:
+            return obj.getState().contains(pyatspi.STATE_FOCUSED)
+
+        return False
+
     def isFocusModeWidget(self, obj):
         try:
             role = obj.getRole()
@@ -2774,6 +2785,23 @@ class Utilities(script_utilities.Utilities):
         self._isMenuDescendant[hash(obj)] = rv
         return rv
 
+    def isNavigableToolTipDescendant(self, obj):
+        if not obj:
+            return False
+
+        rv = self._isNavigableToolTipDescendant.get(hash(obj))
+        if rv is not None:
+            return rv
+
+        isToolTip = lambda x: x and x.getRole() == pyatspi.ROLE_TOOL_TIP
+        if isToolTip(obj):
+            ancestor = obj
+        else:
+            ancestor = pyatspi.findAncestor(obj, isToolTip)
+        rv = ancestor and not self.isNonNavigablePopup(ancestor)
+        self._isNavigableToolTipDescendant[hash(obj)] = rv
+        return rv
+
     def isToolBarDescendant(self, obj):
         if not obj:
             return False
@@ -3601,7 +3629,8 @@ class Utilities(script_utilities.Utilities):
         if rv is not None:
             return rv
 
-        rv = obj.getRole() == pyatspi.ROLE_TOOL_TIP
+        rv = obj.getRole() == pyatspi.ROLE_TOOL_TIP \
+            and not obj.getState().contains(pyatspi.STATE_FOCUSABLE)
 
         self._isNonNavigablePopup[hash(obj)] = rv
         return rv
diff --git a/src/orca/scripts/web/speech_generator.py b/src/orca/scripts/web/speech_generator.py
index eaf824d84..ab91d7c84 100644
--- a/src/orca/scripts/web/speech_generator.py
+++ b/src/orca/scripts/web/speech_generator.py
@@ -83,7 +83,8 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
 
         if self._script.utilities.isLink(obj) \
            or self._script.utilities.isLandmark(obj) \
-           or self._script.utilities.isMath(obj):
+           or self._script.utilities.isMath(obj) \
+           or obj.getRole() == pyatspi.ROLE_TOOL_TIP:
             return result
 
         args['stopAtRoles'] = [pyatspi.ROLE_DOCUMENT_FRAME,
diff --git a/src/orca/speech_generator.py b/src/orca/speech_generator.py
index a2bb77c29..36eeaacbb 100644
--- a/src/orca/speech_generator.py
+++ b/src/orca/speech_generator.py
@@ -1756,13 +1756,14 @@ class SpeechGenerator(generator.Generator):
                     'ROLE_CONTENT_INSERTION',
                     'ROLE_CONTENT_MARK',
                     'ROLE_CONTENT_SUGGESTION',
-                    pyatspi.ROLE_FORM,
-                    pyatspi.ROLE_LANDMARK,
                     'ROLE_DPUB_LANDMARK',
                     'ROLE_DPUB_SECTION',
+                    pyatspi.ROLE_FORM,
+                    pyatspi.ROLE_LANDMARK,
                     pyatspi.ROLE_LIST,
                     pyatspi.ROLE_PANEL,
-                    pyatspi.ROLE_TABLE]
+                    pyatspi.ROLE_TABLE,
+                    pyatspi.ROLE_TOOL_TIP]
 
         enabled, disabled = [], []
         if self._script.inSayAll():
@@ -1774,6 +1775,7 @@ class SpeechGenerator(generator.Generator):
                 enabled.append(pyatspi.ROLE_LIST)
             if _settingsManager.getSetting('sayAllContextPanel'):
                 enabled.extend([pyatspi.ROLE_PANEL,
+                                pyatspi.ROLE_TOOL_TIP,
                                 'ROLE_CONTENT_DELETION',
                                 'ROLE_CONTENT_INSERTION',
                                 'ROLE_CONTENT_MARK',
@@ -1792,6 +1794,7 @@ class SpeechGenerator(generator.Generator):
                 enabled.append(pyatspi.ROLE_LIST)
             if _settingsManager.getSetting('speakContextPanel'):
                 enabled.extend([pyatspi.ROLE_PANEL,
+                                pyatspi.ROLE_TOOL_TIP,
                                 'ROLE_CONTENT_DELETION',
                                 'ROLE_CONTENT_INSERTION',
                                 'ROLE_CONTENT_MARK',
@@ -1917,6 +1920,8 @@ class SpeechGenerator(generator.Generator):
                 result = ['']
         elif role == pyatspi.ROLE_FORM:
             result.append(messages.LEAVING_FORM)
+        elif role == pyatspi.ROLE_TOOL_TIP:
+            result.append(messages.LEAVING_TOOL_TIP)
         elif role == 'ROLE_CONTENT_DELETION':
             result.append(messages.CONTENT_DELETION_END)
         elif role == 'ROLE_CONTENT_INSERTION':
@@ -1979,6 +1984,9 @@ class SpeechGenerator(generator.Generator):
         stopAtRoles = args.get('stopAtRoles', [])
         stopAtRoles.extend([pyatspi.ROLE_APPLICATION, pyatspi.ROLE_MENU_BAR])
 
+        stopAfterRoles = args.get('stopAfterRoles', [])
+        stopAfterRoles.extend([pyatspi.ROLE_TOOL_TIP])
+
         presentOnce = [pyatspi.ROLE_BLOCK_QUOTE, pyatspi.ROLE_LIST]
 
         presentCommonAncestor = False
@@ -2010,7 +2018,7 @@ class SpeechGenerator(generator.Generator):
                 ancestors.append(parent)
                 ancestorRoles.append(parentRole)
 
-            if parent == commonAncestor:
+            if parent == commonAncestor or parentRole in stopAfterRoles:
                 break
 
             parent = parent.parent
@@ -2069,7 +2077,8 @@ class SpeechGenerator(generator.Generator):
                                'ROLE_DPUB_SECTION',
                                pyatspi.ROLE_LIST,
                                pyatspi.ROLE_PANEL,
-                               pyatspi.ROLE_TABLE]
+                               pyatspi.ROLE_TABLE,
+                               pyatspi.ROLE_TOOL_TIP]
 
         result = []
         if self._script.utilities.isBlockquote(priorObj):


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