[orca] Fix several issues related to rich-text editors in web apps



commit 7226fb5fda0abaf622d1d234dc632d25c6a58bfa
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Wed Mar 11 19:33:09 2020 -0400

    Fix several issues related to rich-text editors in web apps
    
    * Treat multi-line elements with role entry as content editable with
      embedded objects if they have at least one text block element as a
      descendant. Needed to identify editors we were failing to treat as
      editors (e.g. Gmail).
    * Don't speak labelOrName when using caret-navigation keys within
      content-editable content with embedded objects. Needed to fix
      chattiness from focus events within the editor.
    * Prevent presentation of objects which are not on the same line
      during navigation within and between lines.

 src/orca/scripts/web/script.py           | 10 ++++-
 src/orca/scripts/web/script_utilities.py | 65 +++++++++++++++++++++++++++-----
 src/orca/scripts/web/speech_generator.py |  5 +++
 3 files changed, 70 insertions(+), 10 deletions(-)
---
diff --git a/src/orca/scripts/web/script.py b/src/orca/scripts/web/script.py
index 0544dc40f..142c2a26d 100644
--- a/src/orca/scripts/web/script.py
+++ b/src/orca/scripts/web/script.py
@@ -840,7 +840,15 @@ class Script(default.Script):
         if not obj:
             return
 
-        contents = self.utilities.getCharacterContentsAtOffset(obj, offset)
+        contents = None
+        if self.utilities.treatAsEndOfLine(obj, offset) and "Text" in pyatspi.listInterfaces(obj):
+            char = obj.queryText().getText(offset, offset + 1)
+            if char == self.EMBEDDED_OBJECT_CHARACTER:
+                char = ""
+            contents = [[obj, offset, offset + 1, char]]
+        else:
+            contents = self.utilities.getCharacterContentsAtOffset(obj, offset)
+
         if not contents:
             return
 
diff --git a/src/orca/scripts/web/script_utilities.py b/src/orca/scripts/web/script_utilities.py
index b411a764b..5f3169105 100644
--- a/src/orca/scripts/web/script_utilities.py
+++ b/src/orca/scripts/web/script_utilities.py
@@ -1160,7 +1160,7 @@ class Utilities(script_utilities.Utilities):
                 debug.println(debug.LEVEL_INFO, msg, True)
                 return string, start, end
 
-        if boundary == pyatspi.TEXT_BOUNDARY_LINE_START and offset == text.characterCount:
+        if boundary == pyatspi.TEXT_BOUNDARY_LINE_START and self.treatAsEndOfLine(obj, offset):
             offset -= 1
             msg = "WEB: Line sought for %s at end of text. Adjusting offset to %i." % (obj, offset)
             debug.println(debug.LEVEL_INFO, msg, True)
@@ -1543,6 +1543,39 @@ class Utilities(script_utilities.Utilities):
                 (i, acc, start, end, string, extents, states, attrs)
             debug.println(debug.LEVEL_INFO, msg, True)
 
+    def treatAsEndOfLine(self, obj, offset):
+        if not self.isContentEditableWithEmbeddedObjects(obj):
+            return False
+
+        if "Text" not in pyatspi.listInterfaces(obj):
+            return False
+
+        if self.isDocument(obj):
+            return False
+
+        text = obj.queryText()
+        if offset == text.characterCount:
+            msg = "WEB: %s offset %i is end of line: offset is characterCount" % (obj, offset)
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return True
+
+        char = text.getText(offset, offset + 1)
+        if char == "\n":
+            msg = "WEB: %s offset %i is end of line: char at offset is newline" % (obj, offset)
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return True
+
+        if char == self.EMBEDDED_OBJECT_CHARACTER:
+            prevExtents = self.getExtents(obj, offset - 1, offset)
+            thisExtents = self.getExtents(obj, offset, offset + 1)
+            sameLine = self.extentsAreOnSameLine(prevExtents, thisExtents)
+            msg = "WEB: %s offset %i is [obj]. Same line: %s Is end of line: %s" % \
+                (obj, offset, sameLine, not sameLine)
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return not sameLine
+
+        return False
+
     def getLineContentsAtOffset(self, obj, offset, layoutMode=None, useCache=True):
         if not obj:
             return []
@@ -1559,7 +1592,11 @@ class Utilities(script_utilities.Utilities):
             layoutMode = _settingsManager.getSetting('layoutMode') or self._script.inFocusMode()
 
         objects = []
-        extents = self.getExtents(obj, offset, offset + 1)
+        if offset > 0 and self.treatAsEndOfLine(obj, offset):
+            extents = self.getExtents(obj, offset - 1, offset)
+        else:
+            extents = self.getExtents(obj, offset, offset + 1)
+
         if self.isInlineListDescendant(obj):
             container = self.listForInlineListDescendant(obj)
             if container:
@@ -4150,9 +4187,11 @@ class Utilities(script_utilities.Utilities):
             debug.println(debug.LEVEL_INFO, msg, True)
             return rv
 
-        isTextBlockRole = role in self._textBlockElementRoles() or self.isLink(obj)
-        if state.contains(pyatspi.STATE_EDITABLE):
-            rv = isTextBlockRole
+        hasTextBlockRole = lambda x: x and x.getRole() in self._textBlockElementRoles()
+        if role == pyatspi.ROLE_ENTRY and state.contains(pyatspi.STATE_MULTI_LINE):
+            rv = pyatspi.findDescendant(obj, hasTextBlockRole)
+        elif state.contains(pyatspi.STATE_EDITABLE):
+            rv = hasTextBlockRole(obj) or self.isLink(obj)
         elif not self.isDocument(obj):
             document = self.getDocumentForObject(obj)
             rv = self.isContentEditableWithEmbeddedObjects(document)
@@ -4526,11 +4565,19 @@ class Utilities(script_utilities.Utilities):
                 return obj, 0
 
         if text and offset >= text.characterCount:
-            if self.isContentEditableWithEmbeddedObjects(obj) and not self.lastInputEventWasLineNav():
+            if self.isContentEditableWithEmbeddedObjects(obj) \
+               and not self.lastInputEventWasLineNav() \
+               and not self.lastInputEventWasLineBoundaryNav():
                 nextObj, nextOffset = self.nextContext(obj, text.characterCount)
-                if nextObj:
-                    msg = "WEB: First caret context at end of %s, %i is next context %s, %i" % \
-                        (obj, offset, nextObj, nextOffset)
+                if not nextObj:
+                    msg = "WEB: No next object found at end of contenteditable %s" % obj
+                    debug.println(debug.LEVEL_INFO, msg, True)
+                elif not self.isContentEditableWithEmbeddedObjects(nextObj):
+                    msg = "WEB: Next object found at end of contenteditable %s is not editable %s" % (obj, 
nextObj)
+                    debug.println(debug.LEVEL_INFO, msg, True)
+                else:
+                    msg = "WEB: First caret context at end of contenteditable %s is next context %s, %i" % \
+                        (obj, nextObj, nextOffset)
                     debug.println(debug.LEVEL_INFO, msg, True)
                     return nextObj, nextOffset
 
diff --git a/src/orca/scripts/web/speech_generator.py b/src/orca/scripts/web/speech_generator.py
index 47e9d89d6..4c1b55031 100644
--- a/src/orca/scripts/web/speech_generator.py
+++ b/src/orca/scripts/web/speech_generator.py
@@ -315,6 +315,11 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
            and not self._script.utilities.isContentSuggestion(obj):
             return []
 
+        if self._script.utilities.isContentEditableWithEmbeddedObjects(obj):
+            lastKey, mods = self._script.utilities.lastKeyAndModifiers()
+            if lastKey in ["Home", "End", "Up", "Down", "Left", "Right", "Page_Up", "Page_Down"]:
+                return []
+
         if obj.name:
             name = obj.name
             if not self._script.utilities.hasExplicitName(obj):


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