[orca] Work around various and sundry cases of brokenness from LibreOffice



commit 9e121010066503b432fb88eae52f95055bd5d161
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Thu Jul 28 00:53:40 2016 -0400

    Work around various and sundry cases of brokenness from LibreOffice

 src/orca/generator.py                              |    2 +
 src/orca/script_utilities.py                       |   50 ++++++-
 src/orca/scripts/apps/soffice/braille_generator.py |   25 ++--
 src/orca/scripts/apps/soffice/formatting.py        |    5 -
 src/orca/scripts/apps/soffice/script.py            |   49 ++++++-
 src/orca/scripts/apps/soffice/script_utilities.py  |  168 ++++++++++++++------
 src/orca/scripts/apps/soffice/speech_generator.py  |    9 +-
 src/orca/where_am_I.py                             |   27 +---
 8 files changed, 237 insertions(+), 98 deletions(-)
---
diff --git a/src/orca/generator.py b/src/orca/generator.py
index 7d62755..0f74b47 100644
--- a/src/orca/generator.py
+++ b/src/orca/generator.py
@@ -1150,5 +1150,7 @@ class Generator:
                 return 'ROLE_MATH_TABLE_ROW'
         if self._script.utilities.isStatic(obj):
             return 'ROLE_STATIC'
+        if self._script.utilities.isFocusableLabel(obj):
+            return pyatspi.ROLE_LIST_ITEM
 
         return args.get('role', obj.getRole())
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index b7949ed..db252f0 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -1043,6 +1043,46 @@ class Utilities:
             isStatic = not obj.getState().contains(pyatspi.STATE_FOCUSABLE)
         return isStatic
 
+    def isFocusableLabel(self, obj):
+        try:
+            role = obj.getRole()
+            state = obj.getState()
+        except:
+            return False
+
+        if role != pyatspi.ROLE_LABEL:
+            return False
+
+        if state.contains(pyatspi.STATE_FOCUSABLE):
+            return True
+
+        if state.contains(pyatspi.STATE_FOCUSED):
+            msg = 'INFO: %s is focused but lacks state focusable' % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return True
+
+        return False
+
+    def isNonFocusableList(self, obj):
+        try:
+            role = obj.getRole()
+            state = obj.getState()
+        except:
+            return False
+
+        if role != pyatspi.ROLE_LIST:
+            return False
+
+        if state.contains(pyatspi.STATE_FOCUSABLE):
+            return False
+
+        if state.contains(pyatspi.STATE_FOCUSED):
+            msg = 'INFO: %s is focused but lacks state focusable' % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return False
+
+        return True
+
     def isStatusBarNotification(self, obj):
         if not (obj and obj.getRole() == pyatspi.ROLE_NOTIFICATION):
             return False
@@ -1081,9 +1121,12 @@ class Utilities:
             attrs = {}
         try:
             role = obj.getRole()
-            parentRole = obj.parent.getRole()
         except:
             role = None
+
+        try:
+            parentRole = obj.parent.getRole()
+        except:
             parentRole = None
 
         try:
@@ -3622,6 +3665,11 @@ class Utilities:
         if not (siblings and obj in siblings):
             return -1, -1
 
+        if self.isFocusableLabel(obj):
+            siblings = list(filter(self.isFocusableLabel, siblings))
+            if len(siblings) == 1:
+                return -1, -1
+
         position = siblings.index(obj)
         setSize = len(siblings)
         return position, setSize
diff --git a/src/orca/scripts/apps/soffice/braille_generator.py 
b/src/orca/scripts/apps/soffice/braille_generator.py
index 7cadcac..c12e8aa 100644
--- a/src/orca/scripts/apps/soffice/braille_generator.py
+++ b/src/orca/scripts/apps/soffice/braille_generator.py
@@ -42,12 +42,13 @@ class BrailleGenerator(braille_generator.BrailleGenerator):
         braille_generator.BrailleGenerator.__init__(self, script)
 
     def _generateRoleName(self, obj, **args):
-        result = []
-        role = args.get('role', obj.getRole())
-        if role != pyatspi.ROLE_DOCUMENT_FRAME:
-            result.extend(braille_generator.BrailleGenerator._generateRoleName(
-                self, obj, **args))
-        return result
+        if self._script.utilities.isDocument(obj):
+            return []
+
+        if self._script.utilities.isFocusableLabel(obj):
+            return []
+
+        return super()._generateRoleName(obj, **args)
 
     def _generateRowHeader(self, obj, **args):
         """Returns an array of strings that represent the row header for an
@@ -211,11 +212,11 @@ class BrailleGenerator(braille_generator.BrailleGenerator):
         return result
 
     def generateBraille(self, obj, **args):
-        result = []
-        args['useDefaultFormatting'] = \
-            ((obj.getRole() == pyatspi.ROLE_LIST) \
-                and (not obj.getState().contains(pyatspi.STATE_FOCUSABLE)))
-        result.extend(braille_generator.BrailleGenerator.\
-                          generateBraille(self, obj, **args))
+        args['useDefaultFormatting'] = self._script.utilities.isNonFocusableList(obj)
+        oldRole = self._overrideRole(self._getAlternativeRole(obj, **args), args)
+
+        result = super().generateBraille(obj, **args)
+
         del args['useDefaultFormatting']
+        self._restoreRole(oldRole, args)
         return result
diff --git a/src/orca/scripts/apps/soffice/formatting.py b/src/orca/scripts/apps/soffice/formatting.py
index b3583a2..441acc4 100644
--- a/src/orca/scripts/apps/soffice/formatting.py
+++ b/src/orca/scripts/apps/soffice/formatting.py
@@ -36,11 +36,6 @@ import orca.settings
 
 formatting = {
     'speech': {
-        pyatspi.ROLE_LABEL: {
-            'focused': 'expandableState + availability',
-            'unfocused': 'name + allTextSelection + expandableState + availability + positionInList',
-            'basicWhereAmI': 'roleName + name + positionInList + expandableState + (nodeLevel or 
nestingLevel)'
-            },
         pyatspi.ROLE_TABLE_CELL: {
             'focused': 'endOfTableIndicator + pause + tableCellRow + pause',
             'unfocused': 'endOfTableIndicator + pause + tableCellRow + pause',
diff --git a/src/orca/scripts/apps/soffice/script.py b/src/orca/scripts/apps/soffice/script.py
index e9c5384..f1b3b3e 100644
--- a/src/orca/scripts/apps/soffice/script.py
+++ b/src/orca/scripts/apps/soffice/script.py
@@ -637,6 +637,11 @@ class Script(default.Script):
     def onActiveChanged(self, event):
         """Callback for object:state-changed:active accessibility events."""
 
+        if not event.source.parent:
+            msg = "SOFFICE: Event source lacks parent"
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return
+
         # Prevent this events from activating the find operation.
         # See comment #18 of bug #354463.
         if self.findCommandRun:
@@ -702,6 +707,16 @@ class Script(default.Script):
         if self.utilities.isSameObject(orca_state.locusOfFocus, event.source):
             return
 
+        if self.utilities.isFocusableLabel(event.source):
+            orca.setLocusOfFocus(event, event.source)
+            return
+
+        if self.utilities.isZombie(event.source):
+            comboBox = self.utilities.containingComboBox(event.source)
+            if comboBox:
+                orca.setLocusOfFocus(event, comboBox, True)
+                return
+
         role = event.source.getRole()
 
         # This seems to be something we inherit from Gtk+
@@ -745,12 +760,19 @@ class Script(default.Script):
             debug.println(debug.LEVEL_INFO, msg, True)
             return
 
+        role = event.source.getRole()
+        if role in [pyatspi.ROLE_TEXT, pyatspi.ROLE_LIST]:
+            comboBox = self.utilities.containingComboBox(event.source)
+            if comboBox:
+                orca.setLocusOfFocus(event, comboBox, True)
+                return
+
         parent = event.source.parent
         if parent and parent.getRole() == pyatspi.ROLE_TOOL_BAR:
             default.Script.onFocusedChanged(self, event)
             return
 
-        role = event.source.getRole()
+        # TODO - JD: Verify this is still needed
         ignoreRoles = [pyatspi.ROLE_FILLER, pyatspi.ROLE_PANEL]
         if role in ignoreRoles:
             return
@@ -775,15 +797,13 @@ class Script(default.Script):
 
         # We should present this in response to active-descendant-changed events
         if event.source.getState().contains(pyatspi.STATE_MANAGES_DESCENDANTS):
-            if role != pyatspi.ROLE_LIST:
-                return
+            return
 
         default.Script.onFocusedChanged(self, event)
 
     def onCaretMoved(self, event):
         """Callback for object:text-caret-moved accessibility events."""
 
-
         if event.detail1 == -1:
             return
 
@@ -854,6 +874,27 @@ class Script(default.Script):
 
         super().onSelectedChanged(event)
 
+    def onSelectionChanged(self, event):
+        """Callback for object:selection-changed accessibility events."""
+
+        if not self.utilities.isComboBoxSelectionChange(event):
+            super().onSelectionChanged(event)
+            return
+
+        selectedChildren = self.utilities.selectedChildren(event.source)
+        if len(selectedChildren) == 1:
+            orca.setLocusOfFocus(event, selectedChildren[0], True)
+
+    def onTextSelectionChanged(self, event):
+        """Callback for object:text-selection-changed accessibility events."""
+
+        if self.utilities.isComboBoxNoise(event):
+            msg = "SOFFICE: Event is believed to be combo box noise"
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return
+
+        super().onTextSelectionChanged(event)
+
     def getTextLineAtCaret(self, obj, offset=None, startOffset=None, endOffset=None):
         """To-be-removed. Returns the string, caretOffset, startOffset."""
 
diff --git a/src/orca/scripts/apps/soffice/script_utilities.py 
b/src/orca/scripts/apps/soffice/script_utilities.py
index 7522725..d78d201 100644
--- a/src/orca/scripts/apps/soffice/script_utilities.py
+++ b/src/orca/scripts/apps/soffice/script_utilities.py
@@ -98,9 +98,6 @@ class Utilities(script_utilities.Utilities):
 
         return text
 
-    def isTextArea(self, obj):
-        return obj and obj.getRole() == pyatspi.ROLE_TEXT
-
     def isCellBeingEdited(self, obj):
         if not obj:
             return False
@@ -260,27 +257,32 @@ class Utilities(script_utilities.Utilities):
         return rowHeader, colHeader
 
     def isSameObject(self, obj1, obj2, comparePaths=False, ignoreNames=False):
-        same = super().isSameObject(obj1, obj2, comparePaths, ignoreNames)
-        if not same or obj1 == obj2:
-            return same
-
-        # The document frame currently contains just the active page,
-        # resulting in false positives. So for paragraphs, rely upon
-        # the equality check.
-        if obj1.getRole() == obj2.getRole() == pyatspi.ROLE_PARAGRAPH:
+        if obj1 == obj2:
+            return True
+
+        try:
+            role1 = obj1.getRole()
+            role2 = obj2.getRole()
+        except:
             return False
 
-        # Handle the case of false positives in dialog boxes resulting
-        # from getIndexInParent() returning a bogus value. bgo#618790.
-        #
-        if not obj1.name \
-           and obj1.getRole() == pyatspi.ROLE_TABLE_CELL \
-           and obj1.getIndexInParent() == obj2.getIndexInParent() == -1:
-            top = self.topLevelObject(obj1)
-            if top and top.getRole() == pyatspi.ROLE_DIALOG:
-                same = False
+        if role1 != role2 or role1 == pyatspi.ROLE_PARAGRAPH:
+            return False
+
+        try:
+            name1 = obj1.name
+            name2 = obj2.name
+        except:
+            return False
+
+        if name1 == name2:
+            if role1 == pyatspi.ROLE_FRAME:
+                return True
+            if role1 == pyatspi.ROLE_TABLE_CELL and not name1:
+                if self.isZombie(obj1) and self.isZombie(obj2):
+                    return False
 
-        return same
+        return super().isSameObject(obj1, obj2, comparePaths, ignoreNames)
 
     def isLayoutOnly(self, obj):
         """Returns True if the given object is a container which has
@@ -301,7 +303,7 @@ class Utilities(script_utilities.Utilities):
            and obj.parent.getRole() == pyatspi.ROLE_COMBO_BOX:
             return True
 
-        return script_utilities.Utilities.isLayoutOnly(self, obj)
+        return super().isLayoutOnly(obj)
 
     def isAnInputLine(self, obj):
         if not obj:
@@ -407,35 +409,45 @@ class Utilities(script_utilities.Utilities):
 
         return results
 
-    def isFunctionalDialog(self, obj):
-        """Returns true if the window is functioning as a dialog."""
+    def validatedTopLevelObject(self, obj):
+        # TODO - JD: We cannot just override topLevelObject() because that will
+        # break flat review access to document content in LO using Gtk+ 3. That
+        # bug seems to be fixed in LO v5.3.0. When that version is released, this
+        # and hopefully other hacks can be removed.
+        window = super().topLevelObject(obj)
+        if not window or window.getIndexInParent() >= 0:
+            return window
+
+        msg = "SOFFICE: %s's window %s has -1 indexInParent" % (obj, window)
+        debug.println(debug.LEVEL_INFO, msg, True)
+
+        for child in self._script.app:
+            if self.isSameObject(child, window):
+                window = child
+                break
 
-        # The OOo Navigator window looks like a dialog, acts like a
-        # dialog, and loses focus requiring the user to know that it's
-        # there and needs Alt+F6ing into.  But officially it's a normal
-        # window.
+        try:
+            index = window.getIndexInParent()
+        except:
+            index = -1
 
-        # There doesn't seem to be (an efficient) top-down equivalent
-        # of utilities.hasMatchingHierarchy(). But OOo documents have
-        # root panes; this thing does not.
-        #
-        rolesList = [pyatspi.ROLE_FRAME,
-                     pyatspi.ROLE_PANEL,
-                     pyatspi.ROLE_PANEL,
-                     pyatspi.ROLE_TOOL_BAR,
-                     pyatspi.ROLE_PUSH_BUTTON]
-
-        if obj.getRole() != rolesList[0]:
-            # We might be looking at the child.
-            #
-            rolesList.pop(0)
+        msg = "SOFFICE: Returning %s (index: %i)" % (window, index)
+        debug.println(debug.LEVEL_INFO, msg, True)
+        return window
 
-        while obj and obj.childCount and len(rolesList):
-            if obj.getRole() != rolesList.pop(0):
-                return False
-            obj = obj[0]
+    def commonAncestor(self, a, b):
+        ancestor = super().commonAncestor(a, b)
+        if ancestor or not (a and b):
+            return ancestor
 
-        return True
+        windowA = self.validatedTopLevelObject(a)
+        windowB = self.validatedTopLevelObject(b)
+        if not self.isSameObject(windowA, windowB):
+            return None
+
+        msg = "SOFFICE: Adjusted ancestor %s and %s to %s" % (a, b, windowA)
+        debug.println(debug.LEVEL_INFO, msg, True)
+        return windowA
 
     def validParent(self, obj):
         """Returns the first valid parent/ancestor of obj. We need to do
@@ -645,6 +657,56 @@ class Utilities(script_utilities.Utilities):
 
         return False
 
+    def containingComboBox(self, obj):
+        isComboBox = lambda x: x and x.getRole() == pyatspi.ROLE_COMBO_BOX
+        if isComboBox(obj):
+            comboBox = obj
+        else:
+            comboBox = pyatspi.findAncestor(obj, isComboBox)
+
+        if not comboBox:
+            return None
+
+        if not self.isZombie(comboBox):
+            return comboBox
+
+        try:
+            parent = comboBox.parent
+        except:
+            pass
+        else:
+            replicant = self.findReplicant(parent, comboBox)
+            if replicant and not self.isZombie(replicant):
+                comboBox = replicant
+
+        return comboBox
+
+    def isComboBoxSelectionChange(self, event):
+        comboBox = self.containingComboBox(event.source)
+        if not comboBox:
+            return False
+
+        lastKey, mods = self.lastKeyAndModifiers()
+        if not lastKey in ["Down", "Up"]:
+            return False
+
+        return True
+
+    def isComboBoxNoise(self, event):
+        role = event.source.getRole()
+        if role == pyatspi.ROLE_TEXT and event.type.startswith("object:text-"):
+            return self.isComboBoxSelectionChange(event)
+
+        return False
+
+    def isPresentableTextChangedEventForLocusOfFocus(self, event):
+        if self.isComboBoxNoise(event):
+            msg = "SOFFICE: Event is believed to be combo box noise"
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return False
+
+        return super().isPresentableTextChangedEventForLocusOfFocus(event)
+
     def isSelectedTextDeletionEvent(self, event):
         if event.type.startswith("object:state-changed:selected") and not event.detail1:
             return self.isDead(orca_state.locusOfFocus) and self.lastInputEventWasDelete()
@@ -665,11 +727,17 @@ class Utilities(script_utilities.Utilities):
         if not obj:
             return []
 
+        role = obj.getRole()
+        isSelection = lambda x: x and 'Selection' in pyatspi.listInterfaces(x)
+        if not isSelection(obj) and role == pyatspi.ROLE_COMBO_BOX:
+            child = pyatspi.findDescendant(obj, isSelection)
+            if child:
+                return super().selectedChildren(child)
+
         # Things only seem broken for certain tables, e.g. the Paths table.
         # TODO - JD: File the LibreOffice bugs and reference them here.
-        if obj.getRole() != pyatspi.ROLE_TABLE \
-           or self.isSpreadSheetCell(obj):
-            return script_utilities.Utilities.selectedChildren(self, obj)
+        if role != pyatspi.ROLE_TABLE or self.isSpreadSheetCell(obj):
+            return super().selectedChildren(obj)
 
         try:
             selection = obj.querySelection()
diff --git a/src/orca/scripts/apps/soffice/speech_generator.py 
b/src/orca/scripts/apps/soffice/speech_generator.py
index 23612e9..703bd32 100644
--- a/src/orca/scripts/apps/soffice/speech_generator.py
+++ b/src/orca/scripts/apps/soffice/speech_generator.py
@@ -484,11 +484,12 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
             # we can use to guess the coordinates.
             #
             args['guessCoordinates'] = obj.getRole() == pyatspi.ROLE_PARAGRAPH
-            result.extend(speech_generator.SpeechGenerator.\
-                                           generateSpeech(self, obj, **args))
+            result.extend(super().generateSpeech(obj, **args))
             del args['guessCoordinates']
             self._restoreRole(oldRole, args)
         else:
-            result.extend(speech_generator.SpeechGenerator.\
-                                           generateSpeech(self, obj, **args))
+            oldRole = self._overrideRole(self._getAlternativeRole(obj, **args), args)
+            result.extend(super().generateSpeech(obj, **args))
+            self._restoreRole(oldRole, args)
+
         return result
diff --git a/src/orca/where_am_I.py b/src/orca/where_am_I.py
index c2ecc5b..02af522 100644
--- a/src/orca/where_am_I.py
+++ b/src/orca/where_am_I.py
@@ -47,28 +47,11 @@ class WhereAmI:
         """We want to treat text objects inside list items and table cells as
         list items and table cells.
         """
-        # [[[TODO: WDW - we purposely omit ROLE_ENTRY here because
-        # there is a bug in realActiveDescendant: it doesn't dive
-        # deep enough into the hierarchy (see comment #12 of bug
-        # #542714).  So, we won't treat entries inside cells as cells
-        # until we're more comfortable with mucking around with
-        # realActiveDescendant.]]]
-        #
-        role = obj.getRole()
-        if role in [pyatspi.ROLE_TEXT,
-                    pyatspi.ROLE_PASSWORD_TEXT,
-                    pyatspi.ROLE_TERMINAL,
-                    pyatspi.ROLE_PARAGRAPH,
-                    pyatspi.ROLE_SECTION,
-                    pyatspi.ROLE_HEADING,
-                    pyatspi.ROLE_DOCUMENT_FRAME]:
-            ancestor = self._script.utilities.ancestorWithRole(
-                obj,
-                [pyatspi.ROLE_TABLE_CELL, pyatspi.ROLE_LIST_ITEM],
-                [pyatspi.ROLE_FRAME])
-            if ancestor \
-               and not self._script.utilities.isLayoutOnly(ancestor.parent):
-                obj = ancestor
+
+        roles = [pyatspi.ROLE_TABLE_CELL, pyatspi.ROLE_LIST_ITEM]
+        ancestor = pyatspi.findAncestor(obj, lambda x: x and x.getRole() in roles)
+        if ancestor and not self._script.utilities.isLayoutOnly(ancestor.parent):
+            obj = ancestor
         return obj
 
     def whereAmI(self, obj, basicOnly):


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