[orca] Work around various and sundry cases of brokenness from LibreOffice
- From: Joanmarie Diggs <joanied src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [orca] Work around various and sundry cases of brokenness from LibreOffice
- Date: Thu, 28 Jul 2016 04:56:09 +0000 (UTC)
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]