1# Orca
2#
3# Copyright 2010 Joanmarie Diggs.
4# Copyright 2014-2015 Igalia, S.L.
5#
6# This library is free software; you can redistribute it and/or
7# modify it under the terms of the GNU Lesser General Public
8# License as published by the Free Software Foundation; either
9# version 2.1 of the License, or (at your option) any later version.
10#
11# This library is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14# Lesser General Public License for more details.
15#
16# You should have received a copy of the GNU Lesser General Public
17# License along with this library; if not, write to the
18# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
19# Boston MA  02110-1301 USA.
20
21__id__        = "$Id$"
22__version__   = "$Revision$"
23__date__      = "$Date$"
24__copyright__ = "Copyright (c) 2010 Joanmarie Diggs." \
25                "Copyright (c) 2014-2015 Igalia, S.L."
26__license__   = "LGPL"
27
28import functools
29import pyatspi
30import re
31import time
32import urllib
33
34from orca import debug
35from orca import input_event
36from orca import messages
37from orca import mouse_review
38from orca import orca
39from orca import orca_state
40from orca import script_utilities
41from orca import script_manager
42from orca import settings
43from orca import settings_manager
44
45_scriptManager = script_manager.getManager()
46_settingsManager = settings_manager.getManager()
47
48
49class Utilities(script_utilities.Utilities):
50
51    def __init__(self, script):
52        super().__init__(script)
53
54        self._objectAttributes = {}
55        self._currentTextAttrs = {}
56        self._caretContexts = {}
57        self._priorContexts = {}
58        self._contextPathsRolesAndNames = {}
59        self._paths = {}
60        self._inDocumentContent = {}
61        self._inTopLevelWebApp = {}
62        self._isTextBlockElement = {}
63        self._isContentEditableWithEmbeddedObjects = {}
64        self._isCodeDescendant = {}
65        self._isEntryDescendant = {}
66        self._isGridDescendant = {}
67        self._isLabelDescendant = {}
68        self._isMenuDescendant = {}
69        self._isNavigableToolTipDescendant = {}
70        self._isToolBarDescendant = {}
71        self._isWebAppDescendant = {}
72        self._isLayoutOnly = {}
73        self._isFocusableWithMathChild = {}
74        self._mathNestingLevel = {}
75        self._isOffScreenLabel = {}
76        self._elementLinesAreSingleChars= {}
77        self._elementLinesAreSingleWords= {}
78        self._hasNoSize = {}
79        self._hasLongDesc = {}
80        self._hasVisibleCaption = {}
81        self._hasDetails = {}
82        self._isDetails = {}
83        self._isNonInteractiveDescendantOfControl = {}
84        self._hasUselessCanvasDescendant = {}
85        self._isClickableElement = {}
86        self._isAnchor = {}
87        self._isEditableComboBox = {}
88        self._isErrorMessage = {}
89        self._isInlineIframeDescendant = {}
90        self._isInlineListItem = {}
91        self._isInlineListDescendant = {}
92        self._isLandmark = {}
93        self._isLink = {}
94        self._isListDescendant = {}
95        self._isNonNavigablePopup = {}
96        self._isNonEntryTextWidget = {}
97        self._isCustomImage = {}
98        self._isUselessImage = {}
99        self._isRedundantSVG = {}
100        self._isUselessEmptyElement = {}
101        self._hasNameAndActionAndNoUsefulChildren = {}
102        self._isNonNavigableEmbeddedDocument = {}
103        self._isParentOfNullChild = {}
104        self._inferredLabels = {}
105        self._labelsForObject = {}
106        self._labelTargets = {}
107        self._displayedLabelText = {}
108        self._mimeType = {}
109        self._preferDescriptionOverName = {}
110        self._shouldFilter = {}
111        self._shouldInferLabelFor = {}
112        self._treatAsTextObject = {}
113        self._treatAsDiv = {}
114        self._currentObjectContents = None
115        self._currentSentenceContents = None
116        self._currentLineContents = None
117        self._currentWordContents = None
118        self._currentCharacterContents = None
119        self._lastQueuedLiveRegionEvent = None
120        self._findContainer = None
121        self._statusBar = None
122        self._validChildRoles = {pyatspi.ROLE_LIST: [pyatspi.ROLE_LIST_ITEM]}
123
124    def _cleanupContexts(self):
125        toRemove = []
126        for key, [obj, offset] in self._caretContexts.items():
127            if self.isZombie(obj):
128                toRemove.append(key)
129
130        for key in toRemove:
131            self._caretContexts.pop(key, None)
132
133    def dumpCache(self, documentFrame=None, preserveContext=False):
134        if not documentFrame or self.isZombie(documentFrame):
135            documentFrame = self.documentFrame()
136
137        context = self._caretContexts.get(hash(documentFrame.parent))
138
139        msg = "WEB: Clearing all cached info for %s" % documentFrame
140        debug.println(debug.LEVEL_INFO, msg, True)
141
142        self._script.structuralNavigation.clearCache(documentFrame)
143        self.clearCaretContext(documentFrame)
144        self.clearCachedObjects()
145
146        if preserveContext and context:
147            msg = "WEB: Preserving context of %s, %i" % (context[0], context[1])
148            debug.println(debug.LEVEL_INFO, msg, True)
149            self._caretContexts[hash(documentFrame.parent)] = context
150
151    def clearCachedObjects(self):
152        debug.println(debug.LEVEL_INFO, "WEB: cleaning up cached objects", True)
153        self._objectAttributes = {}
154        self._inDocumentContent = {}
155        self._inTopLevelWebApp = {}
156        self._isTextBlockElement = {}
157        self._isContentEditableWithEmbeddedObjects = {}
158        self._isCodeDescendant = {}
159        self._isEntryDescendant = {}
160        self._isGridDescendant = {}
161        self._isLabelDescendant = {}
162        self._isMenuDescendant = {}
163        self._isNavigableToolTipDescendant = {}
164        self._isToolBarDescendant = {}
165        self._isWebAppDescendant = {}
166        self._isLayoutOnly = {}
167        self._isFocusableWithMathChild = {}
168        self._mathNestingLevel = {}
169        self._isOffScreenLabel = {}
170        self._elementLinesAreSingleChars= {}
171        self._elementLinesAreSingleWords= {}
172        self._hasNoSize = {}
173        self._hasLongDesc = {}
174        self._hasVisibleCaption = {}
175        self._hasDetails = {}
176        self._isDetails = {}
177        self._hasUselessCanvasDescendant = {}
178        self._isNonInteractiveDescendantOfControl = {}
179        self._isClickableElement = {}
180        self._isAnchor = {}
181        self._isEditableComboBox = {}
182        self._isErrorMessage = {}
183        self._isInlineIframeDescendant = {}
184        self._isInlineListItem = {}
185        self._isInlineListDescendant = {}
186        self._isLandmark = {}
187        self._isLink = {}
188        self._isListDescendant = {}
189        self._isNonNavigablePopup = {}
190        self._isNonEntryTextWidget = {}
191        self._isCustomImage = {}
192        self._isUselessImage = {}
193        self._isRedundantSVG = {}
194        self._isUselessEmptyElement = {}
195        self._hasNameAndActionAndNoUsefulChildren = {}
196        self._isNonNavigableEmbeddedDocument = {}
197        self._isParentOfNullChild = {}
198        self._inferredLabels = {}
199        self._labelsForObject = {}
200        self._labelTargets = {}
201        self._displayedLabelText = {}
202        self._mimeType = {}
203        self._preferDescriptionOverName = {}
204        self._shouldFilter = {}
205        self._shouldInferLabelFor = {}
206        self._treatAsTextObject = {}
207        self._treatAsDiv = {}
208        self._paths = {}
209        self._contextPathsRolesAndNames = {}
210        self._cleanupContexts()
211        self._priorContexts = {}
212        self._lastQueuedLiveRegionEvent = None
213        self._findContainer = None
214        self._statusBar = None
215
216    def clearContentCache(self):
217        self._currentObjectContents = None
218        self._currentSentenceContents = None
219        self._currentLineContents = None
220        self._currentWordContents = None
221        self._currentCharacterContents = None
222        self._currentTextAttrs = {}
223
224    def isDocument(self, obj):
225        if not obj:
226            return False
227
228        roles = [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_DOCUMENT_WEB, pyatspi.ROLE_EMBEDDED]
229
230        try:
231            rv = obj.getRole() in roles
232        except:
233            msg = "WEB: Exception getting role for %s" % obj
234            debug.println(debug.LEVEL_INFO, msg, True)
235            rv = False
236
237        return rv
238
239    def inDocumentContent(self, obj=None):
240        if not obj:
241            obj = orca_state.locusOfFocus
242
243        if self.isDocument(obj):
244            return True
245
246        rv = self._inDocumentContent.get(hash(obj))
247        if rv is not None:
248            return rv
249
250        document = self.getDocumentForObject(obj)
251        rv = document is not None
252        self._inDocumentContent[hash(obj)] = rv
253        return rv
254
255    def _getDocumentsEmbeddedBy(self, frame):
256        if not frame:
257            return []
258
259        isEmbeds = lambda r: r.getRelationType() == pyatspi.RELATION_EMBEDS
260        try:
261            relations = list(filter(isEmbeds, frame.getRelationSet()))
262        except:
263            msg = "ERROR: Exception getting embeds relation for %s" % frame
264            debug.println(debug.LEVEL_INFO, msg, True)
265            return []
266
267        if not relations:
268            return []
269
270        relation = relations[0]
271        targets = [relation.getTarget(i) for i in range(relation.getNTargets())]
272        if not targets:
273            return []
274
275        return list(filter(self.isDocument, targets))
276
277    def sanityCheckActiveWindow(self):
278        app = self._script.app
279        try:
280            windowInApp = orca_state.activeWindow in app
281        except:
282            msg = "ERROR: Exception checking if %s is in %s" % (orca_state.activeWindow, app)
283            debug.println(debug.LEVEL_INFO, msg, True)
284            windowInApp = False
285
286        if windowInApp:
287            return True
288
289        msg = "WARNING: %s is not in %s" % (orca_state.activeWindow, app)
290        debug.println(debug.LEVEL_INFO, msg, True)
291
292        try:
293            script = _scriptManager.getScript(app, orca_state.activeWindow)
294            msg = "WEB: Script for active Window is %s" % script
295            debug.println(debug.LEVEL_INFO, msg, True)
296        except:
297            msg = "ERROR: Exception getting script for active window"
298            debug.println(debug.LEVEL_INFO, msg, True)
299        else:
300            if type(script) == type(self._script):
301                attrs = script.getTransferableAttributes()
302                for attr, value in attrs.items():
303                    msg = "WEB: Setting %s to %s" % (attr, value)
304                    debug.println(debug.LEVEL_INFO, msg, True)
305                    setattr(self._script, attr, value)
306
307        window = self.activeWindow(app)
308        try:
309            self._script.app = window.getApplication()
310            msg = "WEB: updating script's app to %s" % self._script.app
311            debug.println(debug.LEVEL_INFO, msg, True)
312        except:
313            msg = "ERROR: Exception getting app for %s" % window
314            debug.println(debug.LEVEL_INFO, msg, True)
315            return False
316
317        orca_state.activeWindow = window
318        return True
319
320    def activeDocument(self, window=None):
321        isShowing = lambda x: x and x.getState().contains(pyatspi.STATE_SHOWING)
322        documents = self._getDocumentsEmbeddedBy(window or orca_state.activeWindow)
323        documents = list(filter(isShowing, documents))
324        if len(documents) == 1:
325            return documents[0]
326        return None
327
328    def documentFrame(self, obj=None):
329        if not obj and self.sanityCheckActiveWindow():
330            document = self.activeDocument()
331            if document:
332                return document
333
334        return self.getDocumentForObject(obj or orca_state.locusOfFocus)
335
336    def documentFrameURI(self, documentFrame=None):
337        documentFrame = documentFrame or self.documentFrame()
338        if documentFrame and not self.isZombie(documentFrame):
339            try:
340                document = documentFrame.queryDocument()
341            except NotImplementedError:
342                msg = "WEB: %s does not implement document interface" % documentFrame
343                debug.println(debug.LEVEL_INFO, msg, True)
344            except:
345                msg = "ERROR: Exception querying document interface of %s" % documentFrame
346                debug.println(debug.LEVEL_INFO, msg, True)
347            else:
348                return document.getAttributeValue('DocURL') or document.getAttributeValue('URI')
349
350        return ""
351
352    def isPlainText(self, documentFrame=None):
353        return self.mimeType(documentFrame) == "text/plain"
354
355    def mimeType(self, documentFrame=None):
356        documentFrame = documentFrame or self.documentFrame()
357        rv = self._mimeType.get(hash(documentFrame))
358        if rv is not None:
359            return rv
360
361        try:
362            document = documentFrame.queryDocument()
363            attrs = dict([attr.split(":", 1) for attr in document.getAttributes()])
364        except NotImplementedError:
365            msg = "WEB: %s does not implement document interface" % documentFrame
366            debug.println(debug.LEVEL_INFO, msg, True)
367        except:
368            msg = "ERROR: Exception getting document attributes of %s" % documentFrame
369            debug.println(debug.LEVEL_INFO, msg, True)
370        else:
371            rv = attrs.get("MimeType")
372            msg = "WEB: MimeType of %s is '%s'" % (documentFrame, rv)
373            self._mimeType[hash(documentFrame)] = rv
374
375        return rv
376
377    def grabFocusWhenSettingCaret(self, obj):
378        try:
379            role = obj.getRole()
380            state = obj.getState()
381            childCount = obj.childCount
382        except:
383            msg = "WEB: Exception getting role, state, and childCount for %s" % obj
384            debug.println(debug.LEVEL_INFO, msg, True)
385            return False
386
387        # To avoid triggering popup lists.
388        if role == pyatspi.ROLE_ENTRY:
389            return False
390
391        if role == pyatspi.ROLE_IMAGE:
392            isLink = lambda x: x and x.getRole() == pyatspi.ROLE_LINK
393            return pyatspi.utils.findAncestor(obj, isLink) is not None
394
395        if role == pyatspi.ROLE_HEADING and childCount == 1:
396            return self.isLink(obj[0])
397
398        return state.contains(pyatspi.STATE_FOCUSABLE)
399
400    def grabFocus(self, obj):
401        try:
402            obj.queryComponent().grabFocus()
403        except NotImplementedError:
404            msg = "WEB: %s does not implement the component interface" % obj
405            debug.println(debug.LEVEL_INFO, msg, True)
406        except:
407            msg = "WEB: Exception grabbing focus on %s" % obj
408            debug.println(debug.LEVEL_INFO, msg, True)
409
410    def setCaretPosition(self, obj, offset, documentFrame=None):
411        if self._script.flatReviewContext:
412            self._script.toggleFlatReviewMode()
413
414        grabFocus = self.grabFocusWhenSettingCaret(obj)
415
416        obj, offset = self.findFirstCaretContext(obj, offset)
417        self.setCaretContext(obj, offset, documentFrame)
418        if self._script.focusModeIsSticky():
419            return
420
421        oldFocus = orca_state.locusOfFocus
422        self.clearTextSelection(oldFocus)
423        orca.setLocusOfFocus(None, obj, notifyScript=False)
424        if grabFocus:
425            self.grabFocus(obj)
426
427        # Don't use queryNonEmptyText() because we need to try to force-update focus.
428        if "Text" in pyatspi.listInterfaces(obj):
429            try:
430                obj.queryText().setCaretOffset(offset)
431            except:
432                msg = "WEB: Exception setting caret to %i in %s" % (offset, obj)
433                debug.println(debug.LEVEL_INFO, msg, True)
434            else:
435                msg = "WEB: Caret set to %i in %s" % (offset, obj)
436                debug.println(debug.LEVEL_INFO, msg, True)
437
438        if self._script.useFocusMode(obj, oldFocus) != self._script.inFocusMode():
439            self._script.togglePresentationMode(None)
440
441        if obj:
442            obj.clearCache()
443
444        # TODO - JD: This is private.
445        self._script._saveFocusedObjectInfo(obj)
446
447    def getNextObjectInDocument(self, obj, documentFrame):
448        if not obj:
449            return None
450
451        for relation in obj.getRelationSet():
452            if relation.getRelationType() == pyatspi.RELATION_FLOWS_TO:
453                return relation.getTarget(0)
454
455        if obj == documentFrame:
456            obj, offset = self.getCaretContext(documentFrame)
457            for child in documentFrame:
458                if self.characterOffsetInParent(child) > offset:
459                    return child
460
461        if obj and obj.childCount:
462            return obj[0]
463
464        nextObj = None
465        while obj and not nextObj:
466            index = obj.getIndexInParent() + 1
467            if 0 < index < obj.parent.childCount:
468                nextObj = obj.parent[index]
469            elif obj.parent != documentFrame:
470                obj = obj.parent
471            else:
472                break
473
474        return nextObj
475
476    def getPreviousObjectInDocument(self, obj, documentFrame):
477        if not obj:
478            return None
479
480        for relation in obj.getRelationSet():
481            if relation.getRelationType() == pyatspi.RELATION_FLOWS_FROM:
482                return relation.getTarget(0)
483
484        if obj == documentFrame:
485            obj, offset = self.getCaretContext(documentFrame)
486            for child in documentFrame:
487                if self.characterOffsetInParent(child) < offset:
488                    return child
489
490        index = obj.getIndexInParent() - 1
491        if not 0 <= index < obj.parent.childCount:
492            obj = obj.parent
493            index = obj.getIndexInParent() - 1
494
495        previousObj = obj.parent[index]
496        while previousObj and previousObj.childCount:
497            previousObj = previousObj[previousObj.childCount - 1]
498
499        return previousObj
500
501    def getTopOfFile(self):
502        return self.findFirstCaretContext(self.documentFrame(), 0)
503
504    def getBottomOfFile(self):
505        obj = self.getLastObjectInDocument(self.documentFrame())
506        offset = 0
507        text = self.queryNonEmptyText(obj)
508        if text:
509            offset = text.characterCount - 1
510
511        while obj:
512            lastobj, lastoffset = self.nextContext(obj, offset)
513            if not lastobj:
514                break
515            obj, offset = lastobj, lastoffset
516
517        return [obj, offset]
518
519    def getLastObjectInDocument(self, documentFrame):
520        try:
521            lastChild = documentFrame[documentFrame.childCount - 1]
522        except:
523            lastChild = documentFrame
524        while lastChild:
525            lastObj = self.getNextObjectInDocument(lastChild, documentFrame)
526            if lastObj and lastObj != lastChild:
527                lastChild = lastObj
528            else:
529                break
530
531        return lastChild
532
533    def objectAttributes(self, obj, useCache=True):
534        if not (obj and self.inDocumentContent(obj)):
535            return super().objectAttributes(obj)
536
537        if useCache:
538            rv = self._objectAttributes.get(hash(obj))
539            if rv is not None:
540                return rv
541
542        try:
543            rv = dict([attr.split(':', 1) for attr in obj.getAttributes()])
544        except:
545            rv = {}
546
547        self._objectAttributes[hash(obj)] = rv
548        return rv
549
550    def getRoleDescription(self, obj, isBraille=False):
551        attrs = self.objectAttributes(obj)
552        rv = attrs.get('roledescription', '')
553        if isBraille:
554            rv = attrs.get('brailleroledescription', rv)
555
556        return rv
557
558    def nodeLevel(self, obj):
559        if not (obj and self.inDocumentContent(obj)):
560            return super().nodeLevel(obj)
561
562        rv = -1
563        if not (self.inMenu(obj) or obj.getRole() == pyatspi.ROLE_HEADING):
564            attrs = self.objectAttributes(obj)
565            # ARIA levels are 1-based; non-web content is 0-based. Be consistent.
566            rv = int(attrs.get('level', 0)) -1
567
568        return rv
569
570    def _shouldCalculatePositionAndSetSize(self, obj):
571        return True
572
573    def getPositionAndSetSize(self, obj, **args):
574        posinset = self.getPositionInSet(obj)
575        setsize = self.getSetSize(obj)
576        if posinset is not None and setsize is not None:
577            # ARIA posinset is 1-based
578            return posinset - 1, setsize
579
580        if self._shouldCalculatePositionAndSetSize(obj):
581            return super().getPositionAndSetSize(obj, **args)
582
583        return -1, -1
584
585    def getPositionInSet(self, obj):
586        attrs = self.objectAttributes(obj, False)
587        position = attrs.get('posinset')
588        if position is not None:
589            return int(position)
590
591        if obj.getRole() == pyatspi.ROLE_TABLE_ROW:
592            rowindex = attrs.get('rowindex')
593            if rowindex is None and obj.childCount:
594                roles = self._cellRoles()
595                cell = pyatspi.findDescendant(obj, lambda x: x and x.getRole() in roles)
596                rowindex = self.objectAttributes(cell, False).get('rowindex')
597
598            if rowindex is not None:
599                return int(rowindex)
600
601        return None
602
603    def getSetSize(self, obj):
604        attrs = self.objectAttributes(obj, False)
605        setsize = attrs.get('setsize')
606        if setsize is not None:
607            return int(setsize)
608
609        if obj.getRole() == pyatspi.ROLE_TABLE_ROW:
610            rows, cols = self.rowAndColumnCount(self.getTable(obj))
611            if rows != -1:
612                return rows
613
614        return None
615
616    def _getID(self, obj):
617        attrs = self.objectAttributes(obj)
618        return attrs.get('id')
619
620    def _getDisplayStyle(self, obj):
621        attrs = self.objectAttributes(obj)
622        return attrs.get('display')
623
624    def _getTag(self, obj):
625        attrs = self.objectAttributes(obj)
626        return attrs.get('tag')
627
628    def _getXMLRoles(self, obj):
629        attrs = self.objectAttributes(obj)
630        return attrs.get('xml-roles', '').split()
631
632    def inFindContainer(self, obj=None):
633        if not obj:
634            obj = orca_state.locusOfFocus
635
636        if self.inDocumentContent(obj):
637            return False
638
639        return super().inFindContainer(obj)
640
641    def isEmpty(self, obj):
642        if not self.isTextBlockElement(obj):
643            return False
644
645        if obj.name:
646            return False
647
648        return self.queryNonEmptyText(obj, False) is None
649
650    def isHidden(self, obj):
651        attrs = self.objectAttributes(obj)
652        return attrs.get('hidden', False)
653
654    def _isOrIsIn(self, child, parent):
655        if not (child and parent):
656            return False
657
658        if child == parent:
659            return True
660
661        return pyatspi.findAncestor(child, lambda x: x == parent)
662
663    def isShowingAndVisible(self, obj):
664        rv = super().isShowingAndVisible(obj)
665        if rv or not self.inDocumentContent(obj):
666            return rv
667
668        if not mouse_review.reviewer.inMouseEvent:
669            if not self._isOrIsIn(orca_state.locusOfFocus, obj):
670                return rv
671
672            msg = "WEB: %s contains locusOfFocus but not showing and visible" % obj
673            debug.println(debug.LEVEL_INFO, msg, True)
674
675        obj.clearCache()
676        rv = super().isShowingAndVisible(obj)
677        if rv:
678            msg = "WEB: Clearing cache fixed state of %s. Missing event?" % obj
679            debug.println(debug.LEVEL_INFO, msg, True)
680
681        return rv
682
683    def isTextArea(self, obj):
684        if not self.inDocumentContent(obj):
685            return super().isTextArea(obj)
686
687        if self.isLink(obj):
688            return False
689
690        try:
691            role = obj.getRole()
692            state = obj.getState()
693        except:
694            msg = "WEB: Exception getting role and state for %s" % obj
695            debug.println(debug.LEVEL_INFO, msg, True)
696            return False
697
698        if role == pyatspi.ROLE_COMBO_BOX \
699           and state.contains(pyatspi.STATE_EDITABLE) \
700           and not obj.childCount:
701            return True
702
703        if role in self._textBlockElementRoles():
704            document = self.getDocumentForObject(obj)
705            if document and document.getState().contains(pyatspi.STATE_EDITABLE):
706                return True
707
708        return super().isTextArea(obj)
709
710    def isReadOnlyTextArea(self, obj):
711        # NOTE: This method is deliberately more conservative than isTextArea.
712        if obj.getRole() != pyatspi.ROLE_ENTRY:
713            return False
714
715        state = obj.getState()
716        readOnly = state.contains(pyatspi.STATE_FOCUSABLE) \
717                   and not state.contains(pyatspi.STATE_EDITABLE)
718
719        return readOnly
720
721    def setCaretOffset(self, obj, characterOffset):
722        self.setCaretPosition(obj, characterOffset)
723        self._script.updateBraille(obj)
724
725    def nextContext(self, obj=None, offset=-1, skipSpace=False):
726        if not obj:
727            obj, offset = self.getCaretContext()
728
729        nextobj, nextoffset = self.findNextCaretInOrder(obj, offset)
730        if skipSpace:
731            text = self.queryNonEmptyText(nextobj)
732            while text and text.getText(nextoffset, nextoffset + 1) in [" ", "\xa0"]:
733                nextobj, nextoffset = self.findNextCaretInOrder(nextobj, nextoffset)
734                text = self.queryNonEmptyText(nextobj)
735
736        return nextobj, nextoffset
737
738    def previousContext(self, obj=None, offset=-1, skipSpace=False):
739        if not obj:
740            obj, offset = self.getCaretContext()
741
742        prevobj, prevoffset = self.findPreviousCaretInOrder(obj, offset)
743        if skipSpace:
744            text = self.queryNonEmptyText(prevobj)
745            while text and text.getText(prevoffset, prevoffset + 1) in [" ", "\xa0"]:
746                prevobj, prevoffset = self.findPreviousCaretInOrder(prevobj, prevoffset)
747                text = self.queryNonEmptyText(prevobj)
748
749        return prevobj, prevoffset
750
751    def lastContext(self, root):
752        offset = 0
753        text = self.queryNonEmptyText(root)
754        if text:
755            offset = text.characterCount - 1
756
757        def _isInRoot(o):
758            return o == root or pyatspi.utils.findAncestor(o, lambda x: x == root)
759
760        obj = root
761        while obj:
762            lastobj, lastoffset = self.nextContext(obj, offset)
763            if not (lastobj and _isInRoot(lastobj)):
764                break
765            obj, offset = lastobj, lastoffset
766
767        return obj, offset
768
769    def contextsAreOnSameLine(self, a, b):
770        if a == b:
771            return True
772
773        aObj, aOffset = a
774        bObj, bOffset = b
775        aExtents = self.getExtents(aObj, aOffset, aOffset + 1)
776        bExtents = self.getExtents(bObj, bOffset, bOffset + 1)
777        return self.extentsAreOnSameLine(aExtents, bExtents)
778
779    @staticmethod
780    def extentsAreOnSameLine(a, b, pixelDelta=5):
781        if a == b:
782            return True
783
784        aX, aY, aWidth, aHeight = a
785        bX, bY, bWidth, bHeight = b
786
787        if aWidth == 0 and aHeight == 0:
788            return bY <= aY <= bY + bHeight
789        if bWidth == 0 and bHeight == 0:
790            return aY <= bY <= aY + aHeight
791
792        highestBottom = min(aY + aHeight, bY + bHeight)
793        lowestTop = max(aY, bY)
794        if lowestTop >= highestBottom:
795            return False
796
797        aMiddle = aY + aHeight / 2
798        bMiddle = bY + bHeight / 2
799        if abs(aMiddle - bMiddle) > pixelDelta:
800            return False
801
802        return True
803
804    @staticmethod
805    def getExtents(obj, startOffset, endOffset):
806        if not obj:
807            return [0, 0, 0, 0]
808
809        result = [0, 0, 0, 0]
810        try:
811            text = obj.queryText()
812            if text.characterCount and 0 <= startOffset < endOffset:
813                result = list(text.getRangeExtents(startOffset, endOffset, 0))
814        except NotImplementedError:
815            pass
816        except:
817            msg = "WEB: Exception getting range extents for %s" % obj
818            debug.println(debug.LEVEL_INFO, msg, True)
819            return [0, 0, 0, 0]
820        else:
821            if result[0] and result[1] and result[2] == 0 and result[3] == 0 \
822               and text.getText(startOffset, endOffset).strip():
823                msg = "WEB: Suspected bogus range extents for %s (chars: %i, %i): %s" % \
824                    (obj, startOffset, endOffset, result)
825                debug.println(debug.LEVEL_INFO, msg, True)
826            elif text.characterCount:
827                return result
828
829        role = obj.getRole()
830        try:
831            parentRole = obj.parent.getRole()
832        except:
833            msg = "WEB: Exception getting role of parent (%s) of %s" % (obj.parent, obj)
834            debug.println(debug.LEVEL_INFO, msg, True)
835            parentRole = None
836
837        if role in [pyatspi.ROLE_MENU, pyatspi.ROLE_LIST_ITEM] \
838           and parentRole in [pyatspi.ROLE_COMBO_BOX, pyatspi.ROLE_LIST_BOX]:
839            try:
840                ext = obj.parent.queryComponent().getExtents(0)
841            except NotImplementedError:
842                msg = "WEB: %s does not implement the component interface" % obj.parent
843                debug.println(debug.LEVEL_INFO, msg, True)
844                return [0, 0, 0, 0]
845            except:
846                msg = "WEB: Exception getting extents for %s" % obj.parent
847                debug.println(debug.LEVEL_INFO, msg, True)
848                return [0, 0, 0, 0]
849        else:
850            try:
851                ext = obj.queryComponent().getExtents(0)
852            except NotImplementedError:
853                msg = "WEB: %s does not implement the component interface" % obj
854                debug.println(debug.LEVEL_INFO, msg, True)
855                return [0, 0, 0, 0]
856            except:
857                msg = "WEB: Exception getting extents for %s" % obj
858                debug.println(debug.LEVEL_INFO, msg, True)
859                return [0, 0, 0, 0]
860
861        return [ext.x, ext.y, ext.width, ext.height]
862
863    def descendantAtPoint(self, root, x, y, coordType=None):
864        if coordType is None:
865            coordType = pyatspi.DESKTOP_COORDS
866
867        result = None
868        if self.isDocument(root):
869            result = self.accessibleAtPoint(root, x, y, coordType)
870
871        if result is None:
872            result = super().descendantAtPoint(root, x, y, coordType)
873
874        if self.isListItemMarker(result) or self.isStaticTextLeaf(result):
875            return result.parent
876
877        return result
878
879    def _preserveTree(self, obj):
880        if not (obj and obj.childCount):
881            return False
882
883        if self.isMathTopLevel(obj):
884            return True
885
886        return False
887
888    def expandEOCs(self, obj, startOffset=0, endOffset=-1):
889        if not self.inDocumentContent(obj):
890            return super().expandEOCs(obj, startOffset, endOffset)
891
892        text = self.queryNonEmptyText(obj)
893        if not text:
894            return ""
895
896        if self._preserveTree(obj):
897            utterances = self._script.speechGenerator.generateSpeech(obj)
898            return self._script.speechGenerator.utterancesToString(utterances)
899
900        return super().expandEOCs(obj, startOffset, endOffset).strip()
901
902    def substring(self, obj, startOffset, endOffset):
903        if not self.inDocumentContent(obj):
904            return super().substring(obj, startOffset, endOffset)
905
906        text = self.queryNonEmptyText(obj)
907        if text:
908            return text.getText(startOffset, endOffset)
909
910        return ""
911
912    def textAttributes(self, acc, offset, get_defaults=False):
913        attrsForObj = self._currentTextAttrs.get(hash(acc)) or {}
914        if offset in attrsForObj:
915            return attrsForObj.get(offset)
916
917        attrs = super().textAttributes(acc, offset, get_defaults)
918        objAttributes = self.objectAttributes(acc, False)
919        for key in self._script.attributeNamesDict.keys():
920            value = objAttributes.get(key)
921            if value is not None:
922                attrs[0][key] = value
923
924        self._currentTextAttrs[hash(acc)] = {offset:attrs}
925        return attrs
926
927    def localizeTextAttribute(self, key, value):
928        if key == "justification" and value == "justify":
929            value = "fill"
930
931        return super().localizeTextAttribute(key, value)
932
933    def findObjectInContents(self, obj, offset, contents, usingCache=False):
934        if not obj or not contents:
935            return -1
936
937        offset = max(0, offset)
938        matches = [x for x in contents if x[0] == obj]
939        match = [x for x in matches if x[1] <= offset < x[2]]
940        if match and match[0] and match[0] in contents:
941            return contents.index(match[0])
942        if not usingCache:
943            match = [x for x in matches if offset == x[2]]
944            if match and match[0] and match[0] in contents:
945                return contents.index(match[0])
946
947        if not self.isTextBlockElement(obj):
948            return -1
949
950        child = self.getChildAtOffset(obj, offset)
951        if child and not self.isTextBlockElement(child):
952            matches = [x for x in contents if x[0] == child]
953            if len(matches) == 1:
954                return contents.index(matches[0])
955
956        return -1
957
958    def findPreviousObject(self, obj):
959        result = super().findPreviousObject(obj)
960        if not (obj and self.inDocumentContent(obj)):
961            return result
962
963        if not (result and self.inDocumentContent(result)):
964            return None
965
966        if self.getTopLevelDocumentForObject(result) != self.getTopLevelDocumentForObject(obj):
967            return None
968
969        msg = "WEB: Previous object for %s is %s." % (obj, result)
970        debug.println(debug.LEVEL_INFO, msg, True)
971        return result
972
973    def findNextObject(self, obj):
974        result = super().findNextObject(obj)
975        if not (obj and self.inDocumentContent(obj)):
976            return result
977
978        if not (result and self.inDocumentContent(result)):
979            return None
980
981        if self.getTopLevelDocumentForObject(result) != self.getTopLevelDocumentForObject(obj):
982            return None
983
984        msg = "WEB: Next object for %s is %s." % (obj, result)
985        debug.println(debug.LEVEL_INFO, msg, True)
986        return result
987
988    def isNonEntryTextWidget(self, obj):
989        rv = self._isNonEntryTextWidget.get(hash(obj))
990        if rv is not None:
991            return rv
992
993        roles = [pyatspi.ROLE_CHECK_BOX,
994                 pyatspi.ROLE_CHECK_MENU_ITEM,
995                 pyatspi.ROLE_MENU,
996                 pyatspi.ROLE_MENU_ITEM,
997                 pyatspi.ROLE_PAGE_TAB,
998                 pyatspi.ROLE_RADIO_MENU_ITEM,
999                 pyatspi.ROLE_RADIO_BUTTON,
1000                 pyatspi.ROLE_PUSH_BUTTON,
1001                 pyatspi.ROLE_TOGGLE_BUTTON]
1002
1003        role = obj.getRole()
1004        if role in roles:
1005            rv = True
1006        elif role == pyatspi.ROLE_LIST_ITEM:
1007            rv = obj.parent.getRole() != pyatspi.ROLE_LIST
1008        elif role == pyatspi.ROLE_TABLE_CELL:
1009            if obj.getState().contains(pyatspi.STATE_EDITABLE):
1010                rv = False
1011            else:
1012                rv = not self.isTextBlockElement(obj)
1013
1014        self._isNonEntryTextWidget[hash(obj)] = rv
1015        return rv
1016
1017    def treatAsTextObject(self, obj, excludeNonEntryTextWidgets=True):
1018        if not obj or self.isDead(obj):
1019            return False
1020
1021        rv = self._treatAsTextObject.get(hash(obj))
1022        if rv is not None:
1023            return rv
1024
1025        rv = "Text" in pyatspi.listInterfaces(obj)
1026        if not rv:
1027            msg = "WEB: %s does not implement text interface" % obj
1028            debug.println(debug.LEVEL_INFO, msg, True)
1029
1030        if not self.inDocumentContent(obj):
1031            return rv
1032
1033        if rv and self._treatObjectAsWhole(obj, -1) and obj.name and not self.isCellWithNameFromHeader(obj):
1034            msg = "WEB: Treating %s as non-text: named object treated as whole." % obj
1035            debug.println(debug.LEVEL_INFO, msg, True)
1036            rv = False
1037
1038        elif rv and not self.isLiveRegion(obj):
1039            doNotQuery = [pyatspi.ROLE_LIST_BOX]
1040            role = obj.getRole()
1041            if rv and role in doNotQuery:
1042                msg = "WEB: Treating %s as non-text due to role." % obj
1043                debug.println(debug.LEVEL_INFO, msg, True)
1044                rv = False
1045            if rv and excludeNonEntryTextWidgets and self.isNonEntryTextWidget(obj):
1046                msg = "WEB: Treating %s as non-text: is non-entry text widget." % obj
1047                debug.println(debug.LEVEL_INFO, msg, True)
1048                rv = False
1049            if rv and (self.isHidden(obj) or self.isOffScreenLabel(obj)):
1050                msg = "WEB: Treating %s as non-text: is hidden or off-screen label." % obj
1051                debug.println(debug.LEVEL_INFO, msg, True)
1052                rv = False
1053            if rv and self.isNonNavigableEmbeddedDocument(obj):
1054                msg = "WEB: Treating %s as non-text: is non-navigable embedded document." % obj
1055                debug.println(debug.LEVEL_INFO, msg, True)
1056                rv = False
1057            if rv and self.isFakePlaceholderForEntry(obj):
1058                msg = "WEB: Treating %s as non-text: is fake placeholder for entry." % obj
1059                debug.println(debug.LEVEL_INFO, msg, True)
1060                rv = False
1061
1062        self._treatAsTextObject[hash(obj)] = rv
1063        return rv
1064
1065    def queryNonEmptyText(self, obj, excludeNonEntryTextWidgets=True):
1066        if self._script.browseModeIsSticky():
1067            return super().queryNonEmptyText(obj)
1068
1069        if not self.treatAsTextObject(obj, excludeNonEntryTextWidgets):
1070            return None
1071
1072        return super().queryNonEmptyText(obj)
1073
1074    def hasNameAndActionAndNoUsefulChildren(self, obj):
1075        if not (obj and self.inDocumentContent(obj)):
1076            return False
1077
1078        rv = self._hasNameAndActionAndNoUsefulChildren.get(hash(obj))
1079        if rv is not None:
1080            return rv
1081
1082        rv = False
1083        if self.hasExplicitName(obj) and "Action" in pyatspi.listInterfaces(obj):
1084            for child in obj:
1085                if not self.isUselessEmptyElement(child) or self.isUselessImage(child):
1086                    break
1087            else:
1088                rv = True
1089
1090        if rv:
1091            msg = "WEB: %s has name and action and no useful children" % obj
1092            debug.println(debug.LEVEL_INFO, msg, True)
1093
1094        self._hasNameAndActionAndNoUsefulChildren[hash(obj)] = rv
1095        return rv
1096
1097    def isNonInteractiveDescendantOfControl(self, obj):
1098        if not (obj and self.inDocumentContent(obj)):
1099            return False
1100
1101        rv = self._isNonInteractiveDescendantOfControl.get(hash(obj))
1102        if rv is not None:
1103            return rv
1104
1105        try:
1106            role = obj.getRole()
1107            state = obj.getState()
1108        except:
1109            msg = "WEB: Exception getting role and state for %s" % obj
1110            debug.println(debug.LEVEL_INFO, msg, True)
1111            return False
1112
1113        rv = False
1114        roles = self._textBlockElementRoles()
1115        roles.extend([pyatspi.ROLE_IMAGE, pyatspi.ROLE_CANVAS])
1116        if role in roles and not state.contains(pyatspi.STATE_FOCUSABLE):
1117            controls = [pyatspi.ROLE_CHECK_BOX,
1118                        pyatspi.ROLE_CHECK_MENU_ITEM,
1119                        pyatspi.ROLE_LIST_BOX,
1120                        pyatspi.ROLE_MENU_ITEM,
1121                        pyatspi.ROLE_RADIO_MENU_ITEM,
1122                        pyatspi.ROLE_RADIO_BUTTON,
1123                        pyatspi.ROLE_PUSH_BUTTON,
1124                        pyatspi.ROLE_TOGGLE_BUTTON,
1125                        pyatspi.ROLE_TREE_ITEM]
1126            rv = pyatspi.findAncestor(obj, lambda x: x and x.getRole() in controls)
1127
1128        self._isNonInteractiveDescendantOfControl[hash(obj)] = rv
1129        return rv
1130
1131    def _treatObjectAsWhole(self, obj, offset=None):
1132        always = [pyatspi.ROLE_CHECK_BOX,
1133                  pyatspi.ROLE_CHECK_MENU_ITEM,
1134                  pyatspi.ROLE_LIST_BOX,
1135                  pyatspi.ROLE_MENU_ITEM,
1136                  pyatspi.ROLE_RADIO_MENU_ITEM,
1137                  pyatspi.ROLE_RADIO_BUTTON,
1138                  pyatspi.ROLE_PUSH_BUTTON,
1139                  pyatspi.ROLE_TOGGLE_BUTTON]
1140
1141        descendable = [pyatspi.ROLE_MENU,
1142                       pyatspi.ROLE_MENU_BAR,
1143                       pyatspi.ROLE_TOOL_BAR,
1144                       pyatspi.ROLE_TREE_ITEM]
1145
1146        role = obj.getRole()
1147        if role in always:
1148            return True
1149
1150        if role in descendable:
1151            if self._script.inFocusMode():
1152                return True
1153
1154            # This should cause us to initially stop at the large containers before
1155            # allowing the user to drill down into them in browse mode.
1156            return offset == -1
1157
1158        if role == pyatspi.ROLE_ENTRY:
1159            if obj.childCount == 1 and self.isFakePlaceholderForEntry(obj[0]):
1160                return True
1161            return False
1162
1163        state = obj.getState()
1164        if state.contains(pyatspi.STATE_EDITABLE):
1165            return False
1166
1167        if role == pyatspi.ROLE_TABLE_CELL:
1168            if self.isFocusModeWidget(obj):
1169                return not self._script.browseModeIsSticky()
1170            if self.hasNameAndActionAndNoUsefulChildren(obj):
1171                return True
1172
1173        if role in [pyatspi.ROLE_COLUMN_HEADER, pyatspi.ROLE_ROW_HEADER] \
1174           and self.hasExplicitName(obj):
1175            return True
1176
1177        if role == pyatspi.ROLE_COMBO_BOX:
1178            return True
1179
1180        if role in [pyatspi.ROLE_EMBEDDED, pyatspi.ROLE_TREE, pyatspi.ROLE_TREE_TABLE]:
1181            return not self._script.browseModeIsSticky()
1182
1183        if role == pyatspi.ROLE_LINK:
1184            return self.hasExplicitName(obj) or self.hasUselessCanvasDescendant(obj)
1185
1186        if self.isNonNavigableEmbeddedDocument(obj):
1187            return True
1188
1189        if self.isFakePlaceholderForEntry(obj):
1190            return True
1191
1192        if self.isCustomImage(obj):
1193            return True
1194
1195        return False
1196
1197    def __findRange(self, text, offset, start, end, boundary):
1198        # We should not have to do any of this. Seriously. This is why
1199        # We can't have nice things.
1200
1201        allText = text.getText(0, -1)
1202        if boundary == pyatspi.TEXT_BOUNDARY_CHAR:
1203            try:
1204                string = allText[offset]
1205            except IndexError:
1206                string = ""
1207
1208            return string, offset, offset + 1
1209
1210        extents = list(text.getRangeExtents(offset, offset + 1, 0))
1211
1212        def _inThisSpan(span):
1213            return span[0] <= offset <= span[1]
1214
1215        def _onThisLine(span):
1216            start, end = span
1217            startExtents = list(text.getRangeExtents(start, start + 1, 0))
1218            endExtents = list(text.getRangeExtents(end - 1, end, 0))
1219            delta = max(startExtents[3], endExtents[3])
1220            if not self.extentsAreOnSameLine(startExtents, endExtents, delta):
1221                msg = "FAIL: Start %s and end %s of '%s' not on same line" \
1222                      % (startExtents, endExtents, allText[start:end])
1223                debug.println(debug.LEVEL_INFO, msg, True)
1224                startExtents = endExtents
1225
1226            return self.extentsAreOnSameLine(extents, startExtents)
1227
1228        spans = []
1229        charCount = text.characterCount
1230        if boundary == pyatspi.TEXT_BOUNDARY_SENTENCE_START:
1231            spans = [m.span() for m in re.finditer(r"\S*[^\.\?\!]+((?<!\w)[\.\?\!]+(?!\w)|\S*)", allText)]
1232        elif boundary is not None:
1233            spans = [m.span() for m in re.finditer("[^\n\r]+", allText)]
1234        if not spans:
1235            spans = [(0, charCount)]
1236
1237        rangeStart, rangeEnd = 0, charCount
1238        for span in spans:
1239            if _inThisSpan(span):
1240                rangeStart, rangeEnd = span[0], span[1] + 1
1241                break
1242
1243        string = allText[rangeStart:rangeEnd]
1244        if string and boundary in [pyatspi.TEXT_BOUNDARY_SENTENCE_START, None]:
1245            return string, rangeStart, rangeEnd
1246
1247        words = [m.span() for m in re.finditer("[^\\s\ufffc]+", string)]
1248        words = list(map(lambda x: (x[0] + rangeStart, x[1] + rangeStart), words))
1249        if boundary == pyatspi.TEXT_BOUNDARY_WORD_START:
1250            spans = list(filter(_inThisSpan, words))
1251        if boundary == pyatspi.TEXT_BOUNDARY_LINE_START:
1252            spans = list(filter(_onThisLine, words))
1253        if spans:
1254            rangeStart, rangeEnd = spans[0][0], spans[-1][1] + 1
1255            string = allText[rangeStart:rangeEnd]
1256
1257        if not (rangeStart <= offset <= rangeEnd):
1258            return allText[start:end], start, end
1259
1260        return string, rangeStart, rangeEnd
1261
1262    def _attemptBrokenTextRecovery(self, obj, **args):
1263        return False
1264
1265    def _getTextAtOffset(self, obj, offset, boundary):
1266        if not obj:
1267            msg = "WEB: Results for text at offset %i for %s using %s:\n" \
1268                  "     String: '', Start: 0, End: 0. (obj is None)" % (offset, obj, boundary)
1269            debug.println(debug.LEVEL_INFO, msg, True)
1270            return '', 0, 0
1271
1272        text = self.queryNonEmptyText(obj)
1273        if not text:
1274            msg = "WEB: Results for text at offset %i for %s using %s:\n" \
1275                  "     String: '', Start: 0, End: 1. (queryNonEmptyText() returned None)" \
1276                  % (offset, obj, boundary)
1277            debug.println(debug.LEVEL_INFO, msg, True)
1278            return '', 0, 1
1279
1280        if boundary is None:
1281            string, start, end = text.getText(0, -1), 0, text.characterCount
1282            s = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1283            msg = "WEB: Results for text at offset %i for %s using %s:\n" \
1284                  "     String: '%s', Start: %i, End: %i." % (offset, obj, boundary, s, start, end)
1285            debug.println(debug.LEVEL_INFO, msg, True)
1286            return string, start, end
1287
1288        if boundary == pyatspi.TEXT_BOUNDARY_SENTENCE_START \
1289            and not obj.getState().contains(pyatspi.STATE_EDITABLE):
1290            allText = text.getText(0, -1)
1291            if obj.getRole() in [pyatspi.ROLE_LIST_ITEM, pyatspi.ROLE_HEADING] \
1292               or not (re.search(r"\w", allText) and self.isTextBlockElement(obj)):
1293                string, start, end = allText, 0, text.characterCount
1294                s = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1295                msg = "WEB: Results for text at offset %i for %s using %s:\n" \
1296                      "     String: '%s', Start: %i, End: %i." % (offset, obj, boundary, s, start, end)
1297                debug.println(debug.LEVEL_INFO, msg, True)
1298                return string, start, end
1299
1300        if boundary == pyatspi.TEXT_BOUNDARY_LINE_START and self.treatAsEndOfLine(obj, offset):
1301            offset -= 1
1302            msg = "WEB: Line sought for %s at end of text. Adjusting offset to %i." % (obj, offset)
1303            debug.println(debug.LEVEL_INFO, msg, True)
1304
1305        offset = max(0, offset)
1306        string, start, end = text.getTextAtOffset(offset, boundary)
1307
1308        # The above should be all that we need to do, but....
1309        if not self._attemptBrokenTextRecovery(obj, boundary=boundary):
1310            s = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1311            msg = "WEB: Results for text at offset %i for %s using %s:\n" \
1312                  "     String: '%s', Start: %i, End: %i.\n" \
1313                  "     Not checking for broken text." % (offset, obj, boundary, s, start, end)
1314            debug.println(debug.LEVEL_INFO, msg, True)
1315            return string, start, end
1316
1317        needSadHack = False
1318        testString, testStart, testEnd = text.getTextAtOffset(start, boundary)
1319        if (string, start, end) != (testString, testStart, testEnd):
1320            s1 = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1321            s2 = testString.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1322            msg = "FAIL: Bad results for text at offset for %s using %s.\n" \
1323                  "      For offset %i - String: '%s', Start: %i, End: %i.\n" \
1324                  "      For offset %i - String: '%s', Start: %i, End: %i.\n" \
1325                  "      The bug is the above results should be the same.\n" \
1326                  "      This very likely needs to be fixed by the toolkit." \
1327                  % (obj, boundary, offset, s1, start, end, start, s2, testStart, testEnd)
1328            debug.println(debug.LEVEL_INFO, msg, True)
1329            needSadHack = True
1330        elif not string and 0 <= offset < text.characterCount:
1331            s1 = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1332            s2 = text.getText(0, -1).replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1333            msg = "FAIL: Bad results for text at offset %i for %s using %s:\n" \
1334                  "      String: '%s', Start: %i, End: %i.\n" \
1335                  "      The bug is no text reported for a valid offset.\n" \
1336                  "      Character count: %i, Full text: '%s'.\n" \
1337                  "      This very likely needs to be fixed by the toolkit." \
1338                  % (offset, obj, boundary, s1, start, end, text.characterCount, s2)
1339            debug.println(debug.LEVEL_INFO, msg, True)
1340            needSadHack = True
1341        elif not (start <= offset < end) and not (self.isPlainText() or self.elementIsPreformattedText(obj)):
1342            s1 = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1343            msg = "FAIL: Bad results for text at offset %i for %s using %s:\n" \
1344                  "      String: '%s', Start: %i, End: %i.\n" \
1345                  "      The bug is the range returned is outside of the offset.\n" \
1346                  "      This very likely needs to be fixed by the toolkit." \
1347                  % (offset, obj, boundary, s1, start, end)
1348            debug.println(debug.LEVEL_INFO, msg, True)
1349            needSadHack = True
1350        elif len(string) < end - start:
1351            s1 = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1352            msg = "FAIL: Bad results for text at offset %i for %s using %s:\n" \
1353                  "      String: '%s', Start: %i, End: %i.\n" \
1354                  "      The bug is that the length of string is less than the text range.\n" \
1355                  "      This very likely needs to be fixed by the toolkit." \
1356                  % (offset, obj, boundary, s1, start, end)
1357            debug.println(debug.LEVEL_INFO, msg, True)
1358            needSadHack = True
1359        elif boundary == pyatspi.TEXT_BOUNDARY_CHAR and string == "\ufffd":
1360            msg = "FAIL: Bad results for text at offset %i for %s using %s:\n" \
1361                  "      String: '%s', Start: %i, End: %i.\n" \
1362                  "      The bug is that we didn't seem to get a valid character.\n" \
1363                  "      This very likely needs to be fixed by the toolkit." \
1364                  % (offset, obj, boundary, string, start, end)
1365            debug.println(debug.LEVEL_INFO, msg, True)
1366            needSadHack = True
1367
1368        if needSadHack:
1369            sadString, sadStart, sadEnd = self.__findRange(text, offset, start, end, boundary)
1370            s = sadString.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1371            msg = "HACK: Attempting to recover from above failure.\n" \
1372                  "      String: '%s', Start: %i, End: %i." % (s, sadStart, sadEnd)
1373            debug.println(debug.LEVEL_INFO, msg, True)
1374            return sadString, sadStart, sadEnd
1375
1376        s = string.replace(self.EMBEDDED_OBJECT_CHARACTER, "[OBJ]").replace("\n", "\\n")
1377        msg = "WEB: Results for text at offset %i for %s using %s:\n" \
1378              "     String: '%s', Start: %i, End: %i." % (offset, obj, boundary, s, start, end)
1379        debug.println(debug.LEVEL_INFO, msg, True)
1380        return string, start, end
1381
1382    def _getContentsForObj(self, obj, offset, boundary):
1383        if not obj:
1384            return []
1385
1386        if boundary == pyatspi.TEXT_BOUNDARY_LINE_START:
1387            if self.isMath(obj):
1388                if self.isMathTopLevel(obj):
1389                    math = obj
1390                else:
1391                    math = self.getMathAncestor(obj)
1392                return [[math, 0, 1, '']]
1393
1394            text = self.queryNonEmptyText(obj)
1395
1396            if self.elementLinesAreSingleChars(obj):
1397                if obj.name and text:
1398                    msg = "WEB: Returning name as contents for %s (single-char lines)" % obj
1399                    debug.println(debug.LEVEL_INFO, msg, True)
1400                    return [[obj, 0, text.characterCount, obj.name]]
1401
1402                msg = "WEB: Returning all text as contents for %s (single-char lines)" % obj
1403                debug.println(debug.LEVEL_INFO, msg, True)
1404                boundary = None
1405
1406            if self.elementLinesAreSingleWords(obj):
1407                if obj.name and text:
1408                    msg = "WEB: Returning name as contents for %s (single-word lines)" % obj
1409                    debug.println(debug.LEVEL_INFO, msg, True)
1410                    return [[obj, 0, text.characterCount, obj.name]]
1411
1412                msg = "WEB: Returning all text as contents for %s (single-word lines)" % obj
1413                debug.println(debug.LEVEL_INFO, msg, True)
1414                boundary = None
1415
1416        role = obj.getRole()
1417        if role == pyatspi.ROLE_INTERNAL_FRAME and obj.childCount == 1:
1418            return self._getContentsForObj(obj[0], 0, boundary)
1419
1420        string, start, end = self._getTextAtOffset(obj, offset, boundary)
1421        if not string:
1422            return [[obj, start, end, string]]
1423
1424        stringOffset = offset - start
1425        try:
1426            char = string[stringOffset]
1427        except:
1428            pass
1429        else:
1430            if char == self.EMBEDDED_OBJECT_CHARACTER:
1431                child = self.getChildAtOffset(obj, offset)
1432                if child:
1433                    return self._getContentsForObj(child, 0, boundary)
1434
1435        ranges = [m.span() for m in re.finditer("[^\ufffc]+", string)]
1436        strings = list(filter(lambda x: x[0] <= stringOffset <= x[1], ranges))
1437        if len(strings) == 1:
1438            rangeStart, rangeEnd = strings[0]
1439            start += rangeStart
1440            string = string[rangeStart:rangeEnd]
1441            end = start + len(string)
1442
1443        return [[obj, start, end, string]]
1444
1445    def getSentenceContentsAtOffset(self, obj, offset, useCache=True):
1446        if not obj:
1447            return []
1448
1449        offset = max(0, offset)
1450
1451        if useCache:
1452            if self.findObjectInContents(obj, offset, self._currentSentenceContents, usingCache=True) != -1:
1453                return self._currentSentenceContents
1454
1455        boundary = pyatspi.TEXT_BOUNDARY_SENTENCE_START
1456        objects = self._getContentsForObj(obj, offset, boundary)
1457        state = obj.getState()
1458        if state.contains(pyatspi.STATE_EDITABLE):
1459            if state.contains(pyatspi.STATE_FOCUSED):
1460                return objects
1461            if self.isContentEditableWithEmbeddedObjects(obj):
1462                return objects
1463
1464        def _treatAsSentenceEnd(x):
1465            xObj, xStart, xEnd, xString = x
1466            if not self.isTextBlockElement(xObj):
1467                return False
1468
1469            text = self.queryNonEmptyText(xObj)
1470            if text and 0 < text.characterCount <= xEnd:
1471                return True
1472
1473            if 0 <= xStart <= 5:
1474                xString = " ".join(xString.split()[1:])
1475
1476            match = re.search(r"\S[\.\!\?]+(\s|\Z)", xString)
1477            return match is not None
1478
1479        # Check for things in the same sentence before this object.
1480        firstObj, firstStart, firstEnd, firstString = objects[0]
1481        while firstObj and firstString:
1482            if self.isTextBlockElement(firstObj):
1483                if firstStart == 0:
1484                    break
1485            elif self.isTextBlockElement(firstObj.parent):
1486                if self.characterOffsetInParent(firstObj) == 0:
1487                    break
1488
1489            prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
1490            onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
1491            onLeft = list(filter(lambda x: x not in objects, onLeft))
1492            endsOnLeft = list(filter(_treatAsSentenceEnd, onLeft))
1493            if endsOnLeft:
1494                i = onLeft.index(endsOnLeft[-1])
1495                onLeft = onLeft[i+1:]
1496
1497            if not onLeft:
1498                break
1499
1500            objects[0:0] = onLeft
1501            firstObj, firstStart, firstEnd, firstString = objects[0]
1502
1503        # Check for things in the same sentence after this object.
1504        while not _treatAsSentenceEnd(objects[-1]):
1505            lastObj, lastStart, lastEnd, lastString = objects[-1]
1506            nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
1507            onRight = self._getContentsForObj(nextObj, nOffset, boundary)
1508            onRight = list(filter(lambda x: x not in objects, onRight))
1509            if not onRight:
1510                break
1511
1512            objects.extend(onRight)
1513
1514        if useCache:
1515            self._currentSentenceContents = objects
1516
1517        return objects
1518
1519    def getCharacterContentsAtOffset(self, obj, offset, useCache=True):
1520        if not obj:
1521            return []
1522
1523        offset = max(0, offset)
1524
1525        if useCache:
1526            if self.findObjectInContents(obj, offset, self._currentCharacterContents, usingCache=True) != -1:
1527                return self._currentCharacterContents
1528
1529        boundary = pyatspi.TEXT_BOUNDARY_CHAR
1530        objects = self._getContentsForObj(obj, offset, boundary)
1531        if useCache:
1532            self._currentCharacterContents = objects
1533
1534        return objects
1535
1536    def getWordContentsAtOffset(self, obj, offset, useCache=True):
1537        if not obj:
1538            return []
1539
1540        offset = max(0, offset)
1541
1542        if useCache:
1543            if self.findObjectInContents(obj, offset, self._currentWordContents, usingCache=True) != -1:
1544                self._debugContentsInfo(obj, offset, self._currentWordContents, "Word (cached)")
1545                return self._currentWordContents
1546
1547        boundary = pyatspi.TEXT_BOUNDARY_WORD_START
1548        objects = self._getContentsForObj(obj, offset, boundary)
1549        extents = self.getExtents(obj, offset, offset + 1)
1550
1551        def _include(x):
1552            if x in objects:
1553                return False
1554
1555            xObj, xStart, xEnd, xString = x
1556            if xStart == xEnd or not xString:
1557                return False
1558
1559            xExtents = self.getExtents(xObj, xStart, xStart + 1)
1560            return self.extentsAreOnSameLine(extents, xExtents)
1561
1562        # Check for things in the same word to the left of this object.
1563        firstObj, firstStart, firstEnd, firstString = objects[0]
1564        prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
1565        while prevObj and firstString and prevObj != firstObj:
1566            text = self.queryNonEmptyText(prevObj)
1567            if not text or text.getText(pOffset, pOffset + 1).isspace():
1568                break
1569
1570            onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
1571            onLeft = list(filter(_include, onLeft))
1572            if not onLeft:
1573                break
1574
1575            if self._contentIsSubsetOf(objects[0], onLeft[-1]):
1576                objects.pop(0)
1577
1578            objects[0:0] = onLeft
1579            firstObj, firstStart, firstEnd, firstString = objects[0]
1580            prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
1581
1582        # Check for things in the same word to the right of this object.
1583        lastObj, lastStart, lastEnd, lastString = objects[-1]
1584        while lastObj and lastString and not lastString[-1].isspace():
1585            nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
1586            if nextObj == lastObj:
1587                break
1588
1589            onRight = self._getContentsForObj(nextObj, nOffset, boundary)
1590            if onRight and self._contentIsSubsetOf(objects[0], onRight[-1]):
1591                onRight = onRight[0:-1]
1592
1593            onRight = list(filter(_include, onRight))
1594            if not onRight:
1595                break
1596
1597            objects.extend(onRight)
1598            lastObj, lastStart, lastEnd, lastString = objects[-1]
1599
1600        # We want to treat the list item marker as its own word.
1601        firstObj, firstStart, firstEnd, firstString = objects[0]
1602        if firstStart == 0 and firstObj.getRole() == pyatspi.ROLE_LIST_ITEM:
1603            objects = [objects[0]]
1604
1605        if useCache:
1606            self._currentWordContents = objects
1607
1608        self._debugContentsInfo(obj, offset, objects, "Word (not cached)")
1609        return objects
1610
1611    def getObjectContentsAtOffset(self, obj, offset=0, useCache=True):
1612        if not obj:
1613            return []
1614
1615        if self.isDead(obj):
1616            msg = "ERROR: Cannot get object contents at offset for dead object."
1617            debug.println(debug.LEVEL_INFO, msg, True)
1618            return []
1619
1620        offset = max(0, offset)
1621
1622        if useCache:
1623            if self.findObjectInContents(obj, offset, self._currentObjectContents, usingCache=True) != -1:
1624                self._debugContentsInfo(obj, offset, self._currentObjectContents, "Object (cached)")
1625                return self._currentObjectContents
1626
1627        objIsLandmark = self.isLandmark(obj)
1628
1629        def _isInObject(x):
1630            if not x:
1631                return False
1632            if x == obj:
1633                return True
1634            return _isInObject(x.parent)
1635
1636        def _include(x):
1637            if x in objects:
1638                return False
1639
1640            xObj, xStart, xEnd, xString = x
1641            if xStart == xEnd:
1642                return False
1643
1644            if objIsLandmark and self.isLandmark(xObj) and obj != xObj:
1645                return False
1646
1647            return _isInObject(xObj)
1648
1649        objects = self._getContentsForObj(obj, offset, None)
1650        lastObj, lastStart, lastEnd, lastString = objects[-1]
1651        nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
1652        while nextObj:
1653            onRight = self._getContentsForObj(nextObj, nOffset, None)
1654            onRight = list(filter(_include, onRight))
1655            if not onRight:
1656                break
1657
1658            objects.extend(onRight)
1659            lastObj, lastEnd = objects[-1][0], objects[-1][2]
1660            nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
1661
1662        if useCache:
1663            self._currentObjectContents = objects
1664
1665        self._debugContentsInfo(obj, offset, objects, "Object (not cached)")
1666        return objects
1667
1668    def _contentIsSubsetOf(self, contentA, contentB):
1669        objA, startA, endA, stringA = contentA
1670        objB, startB, endB, stringB = contentB
1671        if objA == objB:
1672            setA = set(range(startA, endA))
1673            setB = set(range(startB, endB))
1674            return setA.issubset(setB)
1675
1676        return False
1677
1678    def _debugContentsInfo(self, obj, offset, contents, contentsMsg=""):
1679        if debug.LEVEL_INFO < debug.debugLevel:
1680            return
1681
1682        msg = "WEB: %s for %s at offset %i:" % (contentsMsg, obj, offset)
1683        debug.println(debug.LEVEL_INFO, msg, True)
1684
1685        indent = " " * 8
1686        for i, (acc, start, end, string) in enumerate(contents):
1687            try:
1688                extents = self.getExtents(acc, start, end)
1689            except:
1690                extents = "(exception)"
1691            msg = "     %i. chars: %i-%i: '%s' extents=%s\n" % (i, start, end, string, extents)
1692            msg += debug.getAccessibleDetails(debug.LEVEL_INFO, acc, indent)
1693            debug.println(debug.LEVEL_INFO, msg, True)
1694
1695    def treatAsEndOfLine(self, obj, offset):
1696        if not self.isContentEditableWithEmbeddedObjects(obj):
1697            return False
1698
1699        if "Text" not in pyatspi.listInterfaces(obj):
1700            return False
1701
1702        if self.isDocument(obj):
1703            return False
1704
1705        text = obj.queryText()
1706        if offset == text.characterCount:
1707            msg = "WEB: %s offset %i is end of line: offset is characterCount" % (obj, offset)
1708            debug.println(debug.LEVEL_INFO, msg, True)
1709            return True
1710
1711        # Do not treat a literal newline char as the end of line. When there is an
1712        # actual newline character present, user agents should give us the right value
1713        # for the line at that offset. Here we are trying to figure out where asking
1714        # for the line at offset will give us the next line rather than the line where
1715        # the cursor is physically blinking.
1716
1717        char = text.getText(offset, offset + 1)
1718        if char == self.EMBEDDED_OBJECT_CHARACTER:
1719            prevExtents = self.getExtents(obj, offset - 1, offset)
1720            thisExtents = self.getExtents(obj, offset, offset + 1)
1721            sameLine = self.extentsAreOnSameLine(prevExtents, thisExtents)
1722            msg = "WEB: %s offset %i is [obj]. Same line: %s Is end of line: %s" % \
1723                (obj, offset, sameLine, not sameLine)
1724            debug.println(debug.LEVEL_INFO, msg, True)
1725            return not sameLine
1726
1727        return False
1728
1729    def getLineContentsAtOffset(self, obj, offset, layoutMode=None, useCache=True):
1730        if not obj:
1731            return []
1732
1733        if self.isDead(obj):
1734            msg = "ERROR: Cannot get line contents at offset for dead object."
1735            debug.println(debug.LEVEL_INFO, msg, True)
1736            return []
1737
1738        offset = max(0, offset)
1739        if obj.getRole() == pyatspi.ROLE_TOOL_BAR and not self._treatObjectAsWhole(obj):
1740            child = self.getChildAtOffset(obj, offset)
1741            if child:
1742                obj = child
1743                offset = 0
1744
1745        if useCache:
1746            if self.findObjectInContents(obj, offset, self._currentLineContents, usingCache=True) != -1:
1747                self._debugContentsInfo(obj, offset, self._currentLineContents, "Line (cached)")
1748                return self._currentLineContents
1749
1750        if layoutMode is None:
1751            layoutMode = _settingsManager.getSetting('layoutMode') or self._script.inFocusMode()
1752
1753        objects = []
1754        if offset > 0 and self.treatAsEndOfLine(obj, offset):
1755            extents = self.getExtents(obj, offset - 1, offset)
1756        else:
1757            extents = self.getExtents(obj, offset, offset + 1)
1758
1759        if self.isInlineListDescendant(obj):
1760            container = self.listForInlineListDescendant(obj)
1761            if container:
1762                extents = self.getExtents(container, 0, 1)
1763
1764        objBanner = pyatspi.findAncestor(obj, self.isLandmarkBanner)
1765
1766        def _include(x):
1767            if x in objects:
1768                return False
1769
1770            xObj, xStart, xEnd, xString = x
1771            if xStart == xEnd:
1772                return False
1773
1774            xExtents = self.getExtents(xObj, xStart, xStart + 1)
1775
1776            if obj != xObj:
1777                if self.isLandmark(obj) and self.isLandmark(xObj):
1778                    return False
1779                if self.isLink(obj) and self.isLink(xObj):
1780                    xObjBanner =  pyatspi.findAncestor(xObj, self.isLandmarkBanner)
1781                    if (objBanner or xObjBanner) and objBanner != xObjBanner:
1782                        return False
1783                    if abs(extents[0] - xExtents[0]) <= 1 and abs(extents[1] - xExtents[1]) <= 1:
1784                        # This happens with dynamic skip links such as found on Wikipedia.
1785                        return False
1786                elif self.isBlockListDescendant(obj) != self.isBlockListDescendant(xObj):
1787                    return False
1788                elif obj.getRole() in [pyatspi.ROLE_TREE, pyatspi.ROLE_TREE_ITEM] \
1789                     and xObj.getRole() in [pyatspi.ROLE_TREE, pyatspi.ROLE_TREE_ITEM]:
1790                    return False
1791                elif obj.getRole() == pyatspi.ROLE_HEADING and self.hasNoSize(obj):
1792                    return False
1793                elif xObj.getRole() == pyatspi.ROLE_HEADING and self.hasNoSize(xObj):
1794                    return False
1795
1796            if self.isMathTopLevel(xObj) or self.isMath(obj):
1797                onSameLine = self.extentsAreOnSameLine(extents, xExtents, extents[3])
1798            elif self.isTextSubscriptOrSuperscript(xObj):
1799                onSameLine = self.extentsAreOnSameLine(extents, xExtents, xExtents[3])
1800            else:
1801                onSameLine = self.extentsAreOnSameLine(extents, xExtents)
1802            return onSameLine
1803
1804        boundary = pyatspi.TEXT_BOUNDARY_LINE_START
1805        objects = self._getContentsForObj(obj, offset, boundary)
1806        if not layoutMode:
1807            if useCache:
1808                self._currentLineContents = objects
1809
1810            self._debugContentsInfo(obj, offset, objects, "Line (not layout mode)")
1811            return objects
1812
1813        firstObj, firstStart, firstEnd, firstString = objects[0]
1814        if (extents[2] == 0 and extents[3] == 0) or self.isMath(firstObj):
1815            extents = self.getExtents(firstObj, firstStart, firstEnd)
1816
1817        lastObj, lastStart, lastEnd, lastString = objects[-1]
1818        if self.isMathTopLevel(lastObj):
1819            lastObj, lastEnd = self.lastContext(lastObj)
1820            lastEnd += 1
1821
1822        document = self.getDocumentForObject(obj)
1823        prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
1824        nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
1825
1826        # Check for things on the same line to the left of this object.
1827        while prevObj and self.getDocumentForObject(prevObj) == document:
1828            text = self.queryNonEmptyText(prevObj)
1829            if text and text.getText(pOffset, pOffset + 1) in [" ", "\xa0"]:
1830                prevObj, pOffset = self.findPreviousCaretInOrder(prevObj, pOffset)
1831
1832            if text and text.getText(pOffset, pOffset + 1) == "\n" and firstObj == prevObj:
1833                break
1834
1835            onLeft = self._getContentsForObj(prevObj, pOffset, boundary)
1836            onLeft = list(filter(_include, onLeft))
1837            if not onLeft:
1838                break
1839
1840            if self._contentIsSubsetOf(objects[0], onLeft[-1]):
1841                objects.pop(0)
1842
1843            objects[0:0] = onLeft
1844            firstObj, firstStart = objects[0][0], objects[0][1]
1845            prevObj, pOffset = self.findPreviousCaretInOrder(firstObj, firstStart)
1846
1847        # Check for things on the same line to the right of this object.
1848        while nextObj and self.getDocumentForObject(nextObj) == document:
1849            text = self.queryNonEmptyText(nextObj)
1850            if text and text.getText(nOffset, nOffset + 1) in [" ", "\xa0"]:
1851                nextObj, nOffset = self.findNextCaretInOrder(nextObj, nOffset)
1852
1853            if text and text.getText(nOffset, nOffset + 1) == "\n" and lastObj == nextObj:
1854                break
1855
1856            onRight = self._getContentsForObj(nextObj, nOffset, boundary)
1857            if onRight and self._contentIsSubsetOf(objects[0], onRight[-1]):
1858                onRight = onRight[0:-1]
1859
1860            onRight = list(filter(_include, onRight))
1861            if not onRight:
1862                break
1863
1864            objects.extend(onRight)
1865            lastObj, lastEnd = objects[-1][0], objects[-1][2]
1866            if self.isMathTopLevel(lastObj):
1867                lastObj, lastEnd = self.lastContext(lastObj)
1868                lastEnd += 1
1869
1870            nextObj, nOffset = self.findNextCaretInOrder(lastObj, lastEnd - 1)
1871
1872        firstObj, firstStart, firstEnd, firstString = objects[0]
1873        if firstString == "\n" and len(objects) > 1:
1874            objects.pop(0)
1875
1876        if useCache:
1877            self._currentLineContents = objects
1878
1879        self._debugContentsInfo(obj, offset, objects, "Line (layout mode)")
1880        return objects
1881
1882    def getPreviousLineContents(self, obj=None, offset=-1, layoutMode=None, useCache=True):
1883        if obj is None:
1884            obj, offset = self.getCaretContext()
1885
1886        msg = "WEB: Current context is: %s, %i (focus: %s)" \
1887              % (obj, offset, orca_state.locusOfFocus)
1888        debug.println(debug.LEVEL_INFO, msg, True)
1889
1890        if obj and self.isZombie(obj):
1891            msg = "WEB: Current context obj %s is zombie. Clearing cache." % obj
1892            debug.println(debug.LEVEL_INFO, msg, True)
1893            self.clearCachedObjects()
1894
1895            obj, offset = self.getCaretContext()
1896            msg = "WEB: Now Current context is: %s, %i" % (obj, offset)
1897            debug.println(debug.LEVEL_INFO, msg, True)
1898
1899        line = self.getLineContentsAtOffset(obj, offset, layoutMode, useCache)
1900        if not (line and line[0]):
1901            return []
1902
1903        firstObj, firstOffset = line[0][0], line[0][1]
1904        msg = "WEB: First context on line is: %s, %i" % (firstObj, firstOffset)
1905        debug.println(debug.LEVEL_INFO, msg, True)
1906
1907        skipSpace = not self.elementIsPreformattedText(firstObj)
1908        obj, offset = self.previousContext(firstObj, firstOffset, skipSpace)
1909        if not obj and firstObj:
1910            msg = "WEB: Previous context is: %s, %i. Trying again." % (obj, offset)
1911            debug.println(debug.LEVEL_INFO, msg, True)
1912            self.clearCachedObjects()
1913            obj, offset = self.previousContext(firstObj, firstOffset, skipSpace)
1914
1915        msg = "WEB: Previous context is: %s, %i" % (obj, offset)
1916        debug.println(debug.LEVEL_INFO, msg, True)
1917
1918        contents = self.getLineContentsAtOffset(obj, offset, layoutMode, useCache)
1919        if not contents:
1920            msg = "WEB: Could not get line contents for %s, %i" % (obj, offset)
1921            debug.println(debug.LEVEL_INFO, msg, True)
1922            return []
1923
1924        if line == contents:
1925            obj, offset = self.previousContext(obj, offset, True)
1926            msg = "WEB: Got same line. Trying again with %s, %i" % (obj, offset)
1927            debug.println(debug.LEVEL_INFO, msg, True)
1928            contents = self.getLineContentsAtOffset(obj, offset, layoutMode, useCache)
1929
1930        if line == contents:
1931            start, end = self.getHyperlinkRange(obj)
1932            msg = "WEB: Got same line. %s has range in %s of %i-%i" % (obj, obj.parent, start, end)
1933            debug.println(debug.LEVEL_INFO, msg, True)
1934            if start >= 0:
1935                obj, offset = self.previousContext(obj.parent, start, True)
1936                msg = "WEB: Trying again with %s, %i" % (obj, offset)
1937                debug.println(debug.LEVEL_INFO, msg, True)
1938                contents = self.getLineContentsAtOffset(obj, offset, layoutMode, useCache)
1939
1940        return contents
1941
1942    def getNextLineContents(self, obj=None, offset=-1, layoutMode=None, useCache=True):
1943        if obj is None:
1944            obj, offset = self.getCaretContext()
1945
1946        msg = "WEB: Current context is: %s, %i (focus: %s)" \
1947              % (obj, offset, orca_state.locusOfFocus)
1948        debug.println(debug.LEVEL_INFO, msg, True)
1949
1950        if obj and self.isZombie(obj):
1951            msg = "WEB: Current context obj %s is zombie. Clearing cache." % obj
1952            debug.println(debug.LEVEL_INFO, msg, True)
1953            self.clearCachedObjects()
1954
1955            obj, offset = self.getCaretContext()
1956            msg = "WEB: Now Current context is: %s, %i" % (obj, offset)
1957            debug.println(debug.LEVEL_INFO, msg, True)
1958
1959        line = self.getLineContentsAtOffset(obj, offset, layoutMode, useCache)
1960        if not (line and line[0]):
1961            return []
1962
1963        lastObj, lastOffset = line[-1][0], line[-1][2] - 1
1964        math = self.getMathAncestor(lastObj)
1965        if math:
1966            lastObj, lastOffset = self.lastContext(math)
1967
1968        msg = "WEB: Last context on line is: %s, %i" % (lastObj, lastOffset)
1969        debug.println(debug.LEVEL_INFO, msg, True)
1970
1971        skipSpace = not self.elementIsPreformattedText(lastObj)
1972        obj, offset = self.nextContext(lastObj, lastOffset, skipSpace)
1973        if not obj and lastObj:
1974            msg = "WEB: Next context is: %s, %i. Trying again." % (obj, offset)
1975            debug.println(debug.LEVEL_INFO, msg, True)
1976            self.clearCachedObjects()
1977            obj, offset = self.nextContext(lastObj, lastOffset, skipSpace)
1978
1979        msg = "WEB: Next context is: %s, %i" % (obj, offset)
1980        debug.println(debug.LEVEL_INFO, msg, True)
1981
1982        contents = self.getLineContentsAtOffset(obj, offset, layoutMode, useCache)
1983        if line == contents:
1984            obj, offset = self.nextContext(obj, offset, True)
1985            msg = "WEB: Got same line. Trying again with %s, %i" % (obj, offset)
1986            debug.println(debug.LEVEL_INFO, msg, True)
1987            contents = self.getLineContentsAtOffset(obj, offset, layoutMode, useCache)
1988
1989        if line == contents:
1990            start, end = self.getHyperlinkRange(obj)
1991            msg = "WEB: Got same line. %s has range in %s of %i-%i" % (obj, obj.parent, start, end)
1992            debug.println(debug.LEVEL_INFO, msg, True)
1993            if end >= 0:
1994                obj, offset = self.nextContext(obj.parent, end, True)
1995                msg = "WEB: Trying again with %s, %i" % (obj, offset)
1996                debug.println(debug.LEVEL_INFO, msg, True)
1997                contents = self.getLineContentsAtOffset(obj, offset, layoutMode, useCache)
1998
1999        if not contents:
2000            msg = "WEB: Could not get line contents for %s, %i" % (obj, offset)
2001            debug.println(debug.LEVEL_INFO, msg, True)
2002            return []
2003
2004        return contents
2005
2006    def updateCachedTextSelection(self, obj):
2007        if not self.inDocumentContent(obj):
2008            super().updateCachedTextSelection(obj)
2009            return
2010
2011        if self.hasPresentableText(obj):
2012            super().updateCachedTextSelection(obj)
2013
2014    def handleTextSelectionChange(self, obj, speakMessage=True):
2015        if not self.inDocumentContent(obj):
2016            return super().handleTextSelectionChange(obj)
2017
2018        oldStart, oldEnd = self._script.pointOfReference.get('selectionAnchorAndFocus', (None, None))
2019        start, end = self._getSelectionAnchorAndFocus(obj)
2020        self._script.pointOfReference['selectionAnchorAndFocus'] = (start, end)
2021
2022        def _cmp(obj1, obj2):
2023            return self.pathComparison(pyatspi.getPath(obj1), pyatspi.getPath(obj2))
2024
2025        oldSubtree = self._getSubtree(oldStart, oldEnd)
2026        if start == oldStart and end == oldEnd:
2027            descendants = oldSubtree
2028        else:
2029            newSubtree = self._getSubtree(start, end)
2030            descendants = sorted(set(oldSubtree).union(newSubtree), key=functools.cmp_to_key(_cmp))
2031
2032        if not descendants:
2033            return False
2034
2035        for descendant in descendants:
2036            if descendant not in (oldStart, oldEnd, start, end) \
2037               and pyatspi.findAncestor(descendant, lambda x: x in descendants):
2038                super().updateCachedTextSelection(descendant)
2039            else:
2040                super().handleTextSelectionChange(descendant, speakMessage)
2041
2042        return True
2043
2044    def inPDFViewer(self, obj=None):
2045        uri = self.documentFrameURI()
2046        return uri.lower().endswith(".pdf")
2047
2048    def inTopLevelWebApp(self, obj=None):
2049        if not obj:
2050            obj = orca_state.locusOfFocus
2051
2052        rv = self._inTopLevelWebApp.get(hash(obj))
2053        if rv is not None:
2054            return rv
2055
2056        document = self.getDocumentForObject(obj)
2057        if not document and self.isDocument(obj):
2058            document = obj
2059
2060        rv = self.isTopLevelWebApp(document)
2061        self._inTopLevelWebApp[hash(obj)] = rv
2062        return rv
2063
2064    def isTopLevelWebApp(self, obj):
2065        try:
2066            role = obj.getRole()
2067        except:
2068            msg = "WEB: Exception getting role for %s" % obj
2069            debug.println(debug.LEVEL_INFO, msg, True)
2070            return False
2071
2072        if role == pyatspi.ROLE_EMBEDDED and not self.getDocumentForObject(obj.parent):
2073            uri = self.documentFrameURI()
2074            rv = bool(uri and uri.startswith("http"))
2075            msg = "WEB: %s is top-level web application: %s (URI: %s)" % (obj, rv, uri)
2076            debug.println(debug.LEVEL_INFO, msg, True)
2077            return rv
2078
2079        return False
2080
2081    def forceBrowseModeForWebAppDescendant(self, obj):
2082        if not self.isWebAppDescendant(obj):
2083            return False
2084
2085        role = obj.getRole()
2086        if role == pyatspi.ROLE_TOOL_TIP:
2087            return obj.getState().contains(pyatspi.STATE_FOCUSED)
2088
2089        if role == pyatspi.ROLE_DOCUMENT_WEB:
2090            return not self.isFocusModeWidget(obj)
2091
2092        return False
2093
2094    def isFocusModeWidget(self, obj):
2095        try:
2096            role = obj.getRole()
2097            state = obj.getState()
2098        except:
2099            msg = "WEB: Exception getting role and state for %s" % obj
2100            debug.println(debug.LEVEL_INFO, msg, True)
2101            return False
2102
2103        if state.contains(pyatspi.STATE_EDITABLE):
2104            msg = "WEB: %s is focus mode widget because it's editable" % obj
2105            debug.println(debug.LEVEL_INFO, msg, True)
2106            return True
2107
2108        if state.contains(pyatspi.STATE_EXPANDABLE) and state.contains(pyatspi.STATE_FOCUSABLE):
2109            msg = "WEB: %s is focus mode widget because it's expandable and focusable" % obj
2110            debug.println(debug.LEVEL_INFO, msg, True)
2111            return True
2112
2113        alwaysFocusModeRoles = [pyatspi.ROLE_COMBO_BOX,
2114                                pyatspi.ROLE_ENTRY,
2115                                pyatspi.ROLE_LIST_BOX,
2116                                pyatspi.ROLE_MENU,
2117                                pyatspi.ROLE_MENU_ITEM,
2118                                pyatspi.ROLE_CHECK_MENU_ITEM,
2119                                pyatspi.ROLE_RADIO_MENU_ITEM,
2120                                pyatspi.ROLE_PAGE_TAB,
2121                                pyatspi.ROLE_PASSWORD_TEXT,
2122                                pyatspi.ROLE_PROGRESS_BAR,
2123                                pyatspi.ROLE_SLIDER,
2124                                pyatspi.ROLE_SPIN_BUTTON,
2125                                pyatspi.ROLE_TOOL_BAR,
2126                                pyatspi.ROLE_TREE_ITEM,
2127                                pyatspi.ROLE_TREE_TABLE,
2128                                pyatspi.ROLE_TREE]
2129
2130        if role in alwaysFocusModeRoles:
2131            msg = "WEB: %s is focus mode widget due to its role" % obj
2132            debug.println(debug.LEVEL_INFO, msg, True)
2133            return True
2134
2135        if role in [pyatspi.ROLE_TABLE_CELL, pyatspi.ROLE_TABLE] \
2136           and self.isLayoutOnly(self.getTable(obj)):
2137            msg = "WEB: %s is not focus mode widget because it's layout only" % obj
2138            debug.println(debug.LEVEL_INFO, msg, True)
2139            return False
2140
2141        if role == pyatspi.ROLE_LIST_ITEM:
2142            rv = pyatspi.findAncestor(obj, lambda x: x and x.getRole() == pyatspi.ROLE_LIST_BOX)
2143            if rv:
2144                msg = "WEB: %s is focus mode widget because it's a listbox descendant" % obj
2145                debug.println(debug.LEVEL_INFO, msg, True)
2146            return rv
2147
2148        if self.isButtonWithPopup(obj):
2149            msg = "WEB: %s is focus mode widget because it's a button with popup" % obj
2150            debug.println(debug.LEVEL_INFO, msg, True)
2151            return True
2152
2153        focusModeRoles = [pyatspi.ROLE_EMBEDDED,
2154                          pyatspi.ROLE_TABLE_CELL,
2155                          pyatspi.ROLE_TABLE]
2156
2157        if role in focusModeRoles \
2158           and not self.isTextBlockElement(obj) \
2159           and not self.hasNameAndActionAndNoUsefulChildren(obj) \
2160           and not self.inPDFViewer(obj):
2161            msg = "WEB: %s is focus mode widget based on presumed functionality" % obj
2162            debug.println(debug.LEVEL_INFO, msg, True)
2163            return True
2164
2165        if self.isGridDescendant(obj):
2166            msg = "WEB: %s is focus mode widget because it's a grid descendant" % obj
2167            debug.println(debug.LEVEL_INFO, msg, True)
2168            return True
2169
2170        if self.isMenuDescendant(obj):
2171            msg = "WEB: %s is focus mode widget because it's a menu descendant" % obj
2172            debug.println(debug.LEVEL_INFO, msg, True)
2173            return True
2174
2175        if self.isToolBarDescendant(obj):
2176            msg = "WEB: %s is focus mode widget because it's a toolbar descendant" % obj
2177            debug.println(debug.LEVEL_INFO, msg, True)
2178            return True
2179
2180        if self.isContentEditableWithEmbeddedObjects(obj):
2181            msg = "WEB: %s is focus mode widget because it's content editable" % obj
2182            debug.println(debug.LEVEL_INFO, msg, True)
2183            return True
2184
2185        return False
2186
2187    def _cellRoles(self):
2188        roles = [pyatspi.ROLE_TABLE_CELL,
2189                 pyatspi.ROLE_TABLE_COLUMN_HEADER,
2190                 pyatspi.ROLE_TABLE_ROW_HEADER,
2191                 pyatspi.ROLE_ROW_HEADER,
2192                 pyatspi.ROLE_COLUMN_HEADER]
2193
2194        return roles
2195
2196    def _textBlockElementRoles(self):
2197        roles = [pyatspi.ROLE_ARTICLE,
2198                 pyatspi.ROLE_CAPTION,
2199                 pyatspi.ROLE_COLUMN_HEADER,
2200                 pyatspi.ROLE_COMMENT,
2201                 pyatspi.ROLE_DEFINITION,
2202                 pyatspi.ROLE_DESCRIPTION_LIST,
2203                 pyatspi.ROLE_DESCRIPTION_TERM,
2204                 pyatspi.ROLE_DESCRIPTION_VALUE,
2205                 pyatspi.ROLE_DOCUMENT_FRAME,
2206                 pyatspi.ROLE_DOCUMENT_WEB,
2207                 pyatspi.ROLE_FOOTER,
2208                 pyatspi.ROLE_FORM,
2209                 pyatspi.ROLE_HEADING,
2210                 pyatspi.ROLE_LIST,
2211                 pyatspi.ROLE_LIST_ITEM,
2212                 pyatspi.ROLE_PARAGRAPH,
2213                 pyatspi.ROLE_ROW_HEADER,
2214                 pyatspi.ROLE_SECTION,
2215                 pyatspi.ROLE_STATIC,
2216                 pyatspi.ROLE_TEXT,
2217                 pyatspi.ROLE_TABLE_CELL]
2218
2219        # Remove this check when we bump dependencies to 2.34
2220        try:
2221            roles.append(pyatspi.ROLE_CONTENT_DELETION)
2222            roles.append(pyatspi.ROLE_CONTENT_INSERTION)
2223        except:
2224            pass
2225
2226        # Remove this check when we bump dependencies to 2.36
2227        try:
2228            roles.append(pyatspi.ROLE_MARK)
2229            roles.append(pyatspi.ROLE_SUGGESTION)
2230        except:
2231            pass
2232
2233        return roles
2234
2235    def mnemonicShortcutAccelerator(self, obj):
2236        attrs = self.objectAttributes(obj)
2237        keys = map(lambda x: x.replace("+", " "), attrs.get("keyshortcuts", "").split(" "))
2238        keys = map(lambda x: x.replace(" ", "+"), map(self.labelFromKeySequence, keys))
2239        rv = ["", " ".join(keys), ""]
2240        if list(filter(lambda x: x, rv)):
2241            return rv
2242
2243        return super().mnemonicShortcutAccelerator(obj)
2244
2245    def unrelatedLabels(self, root, onlyShowing=True, minimumWords=3):
2246        if not (root and self.inDocumentContent(root)):
2247            return super().unrelatedLabels(root, onlyShowing, minimumWords)
2248
2249        return []
2250
2251    def isFocusableWithMathChild(self, obj):
2252        if not (obj and self.inDocumentContent(obj)):
2253            return False
2254
2255        rv = self._isFocusableWithMathChild.get(hash(obj))
2256        if rv is not None:
2257            return rv
2258
2259        try:
2260            state = obj.getState()
2261        except:
2262            msg = "WEB: Exception getting state for %s" % obj
2263            debug.println(debug.LEVEL_INFO, msg, True)
2264            return False
2265
2266        rv = False
2267        if state.contains(pyatspi.STATE_FOCUSABLE) and not self.isDocument(obj):
2268            for child in obj:
2269                if self.isMathTopLevel(child):
2270                    rv = True
2271                    break
2272
2273        self._isFocusableWithMathChild[hash(obj)] = rv
2274        return rv
2275
2276    def isFocusedWithMathChild(self, obj):
2277        if not self.isFocusableWithMathChild(obj):
2278            return False
2279
2280        try:
2281            state = obj.getState()
2282        except:
2283            msg = "WEB: Exception getting state for %s" % obj
2284            debug.println(debug.LEVEL_INFO, msg, True)
2285            return False
2286
2287        return state.contains(pyatspi.STATE_FOCUSED)
2288
2289    def isTextBlockElement(self, obj):
2290        if not (obj and self.inDocumentContent(obj)):
2291            return False
2292
2293        rv = self._isTextBlockElement.get(hash(obj))
2294        if rv is not None:
2295            return rv
2296
2297        try:
2298            role = obj.getRole()
2299            state = obj.getState()
2300            interfaces = pyatspi.listInterfaces(obj)
2301        except:
2302            msg = "WEB: Exception getting role and state for %s" % obj
2303            debug.println(debug.LEVEL_INFO, msg, True)
2304            return False
2305
2306        textBlockElements = self._textBlockElementRoles()
2307        if not role in textBlockElements:
2308            rv = False
2309        elif not "Text" in interfaces:
2310            rv = False
2311        elif state.contains(pyatspi.STATE_EDITABLE):
2312            rv = False
2313        elif self.isGridCell(obj):
2314            rv = False
2315        elif role in [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_DOCUMENT_WEB]:
2316            rv = True
2317        elif self.isCustomImage(obj):
2318            rv = False
2319        elif not state.contains(pyatspi.STATE_FOCUSABLE) and not state.contains(pyatspi.STATE_FOCUSED):
2320            rv = not self.hasNameAndActionAndNoUsefulChildren(obj)
2321        else:
2322            rv = False
2323
2324        self._isTextBlockElement[hash(obj)] = rv
2325        return rv
2326
2327    def _advanceCaretInEmptyObject(self, obj):
2328        role = obj.getRole()
2329        if role == pyatspi.ROLE_TABLE_CELL and not self.queryNonEmptyText(obj):
2330            return not self._script._lastCommandWasStructNav
2331
2332        return True
2333
2334    def textAtPoint(self, obj, x, y, coordType=None, boundary=None):
2335        if coordType is None:
2336            coordType = pyatspi.DESKTOP_COORDS
2337
2338        if boundary is None:
2339            boundary = pyatspi.TEXT_BOUNDARY_LINE_START
2340
2341        string, start, end = super().textAtPoint(obj, x, y, coordType, boundary)
2342        if string == self.EMBEDDED_OBJECT_CHARACTER:
2343            child = self.getChildAtOffset(obj, start)
2344            if child:
2345                return self.textAtPoint(child, x, y, coordType, boundary)
2346
2347        return string, start, end
2348
2349    def _treatAlertsAsDialogs(self):
2350        return False
2351
2352    def treatAsDiv(self, obj, offset=None):
2353        if not (obj and self.inDocumentContent(obj)):
2354            return False
2355
2356        try:
2357            role = obj.getRole()
2358            childCount = obj.childCount
2359        except:
2360            msg = "WEB: Exception getting role and childCount for %s" % obj
2361            debug.println(debug.LEVEL_INFO, msg, True)
2362            return False
2363
2364        if role == pyatspi.ROLE_LIST and offset is not None:
2365            string = self.substring(obj, offset, offset + 1)
2366            if string and string != self.EMBEDDED_OBJECT_CHARACTER:
2367                return True
2368
2369        if role == pyatspi.ROLE_PANEL and not childCount:
2370            return True
2371
2372        rv = self._treatAsDiv.get(hash(obj))
2373        if rv is not None:
2374            return rv
2375
2376        validRoles = self._validChildRoles.get(role)
2377        if validRoles:
2378            if not childCount:
2379                rv = True
2380            else:
2381                rv = bool([x for x in obj if x and x.getRole() not in validRoles])
2382
2383        if not rv:
2384            validRoles = self._validChildRoles.get(obj.parent)
2385            if validRoles:
2386                rv = bool([x for x in obj.parent if x and x.getRole() not in validRoles])
2387
2388        self._treatAsDiv[hash(obj)] = rv
2389        return rv
2390
2391    def isAriaAlert(self, obj):
2392        return 'alert' in self._getXMLRoles(obj)
2393
2394    def isBlockquote(self, obj):
2395        if super().isBlockquote(obj):
2396            return True
2397
2398        return self._getTag(obj) == 'blockquote'
2399
2400    def isComment(self, obj):
2401        if not (obj and self.inDocumentContent(obj)):
2402            return super().isComment(obj)
2403
2404        if obj.getRole() == pyatspi.ROLE_COMMENT:
2405            return True
2406
2407        return 'comment' in self._getXMLRoles(obj)
2408
2409    def isContentDeletion(self, obj):
2410        if not (obj and self.inDocumentContent(obj)):
2411            return super().isContentDeletion(obj)
2412
2413        # Remove this check when we bump dependencies to 2.34
2414        try:
2415            if obj.getRole() == pyatspi.ROLE_CONTENT_DELETION:
2416                return True
2417        except:
2418            pass
2419
2420        return 'deletion' in self._getXMLRoles(obj) or 'del' == self._getTag(obj)
2421
2422    def isContentError(self, obj):
2423        if not (obj and self.inDocumentContent(obj)):
2424            return super().isContentError(obj)
2425
2426        if obj.getRole() not in self._textBlockElementRoles():
2427            return False
2428
2429        return obj.getState().contains(pyatspi.STATE_INVALID_ENTRY)
2430
2431    def isContentInsertion(self, obj):
2432        if not (obj and self.inDocumentContent(obj)):
2433            return super().isContentInsertion(obj)
2434
2435        # Remove this check when we bump dependencies to 2.34
2436        try:
2437            if obj.getRole() == pyatspi.ROLE_CONTENT_INSERTION:
2438                return True
2439        except:
2440            pass
2441
2442        return 'insertion' in self._getXMLRoles(obj) or 'ins' == self._getTag(obj)
2443
2444    def isContentMarked(self, obj):
2445        if not (obj and self.inDocumentContent(obj)):
2446            return super().isContentMarked(obj)
2447
2448        # Remove this check when we bump dependencies to 2.36
2449        try:
2450            if obj.getRole() == pyatspi.ROLE_MARK:
2451                return True
2452        except:
2453            pass
2454
2455        return 'mark' in self._getXMLRoles(obj) or 'mark' == self._getTag(obj)
2456
2457    def isContentSuggestion(self, obj):
2458        if not (obj and self.inDocumentContent(obj)):
2459            return super().isContentSuggestion(obj)
2460
2461        # Remove this check when we bump dependencies to 2.36
2462        try:
2463            if obj.getRole() == pyatspi.ROLE_SUGGESTION:
2464                return True
2465        except:
2466            pass
2467
2468        return 'suggestion' in self._getXMLRoles(obj)
2469
2470    def isCustomElement(self, obj):
2471        tag = self._getTag(obj)
2472        return tag and '-' in tag
2473
2474    def isInlineIframe(self, obj):
2475        if not (obj and obj.getRole() == pyatspi.ROLE_INTERNAL_FRAME):
2476            return False
2477
2478        displayStyle = self._getDisplayStyle(obj)
2479        if "inline" not in displayStyle:
2480            return False
2481
2482        return self.documentForObject(obj) is not None
2483
2484    def isInlineIframeDescendant(self, obj):
2485        if not obj:
2486            return False
2487
2488        rv = self._isInlineIframeDescendant.get(hash(obj))
2489        if rv is not None:
2490            return rv
2491
2492        ancestor = pyatspi.findAncestor(obj, self.isInlineIframe)
2493        rv = ancestor is not None
2494        self._isInlineIframeDescendant[hash(obj)] = rv
2495        return rv
2496
2497    def isInlineSuggestion(self, obj):
2498        if not self.isContentSuggestion(obj):
2499            return False
2500
2501        displayStyle = self._getDisplayStyle(obj)
2502        return "inline" in displayStyle
2503
2504    def isFirstItemInInlineContentSuggestion(self, obj):
2505        suggestion = pyatspi.findAncestor(obj, self.isInlineSuggestion)
2506        if not (suggestion and suggestion.childCount):
2507            return False
2508
2509        return suggestion[0] == obj
2510
2511    def isLastItemInInlineContentSuggestion(self, obj):
2512        suggestion = pyatspi.findAncestor(obj, self.isInlineSuggestion)
2513        if not (suggestion and suggestion.childCount):
2514            return False
2515
2516        return suggestion[-1] == obj
2517
2518    def speakMathSymbolNames(self, obj=None):
2519        obj = obj or orca_state.locusOfFocus
2520        return self.isMath(obj)
2521
2522    def isInMath(self):
2523        return self.isMath(orca_state.locusOfFocus)
2524
2525    def isMath(self, obj):
2526        tag = self._getTag(obj)
2527        rv = tag in ['math',
2528                     'maction',
2529                     'maligngroup',
2530                     'malignmark',
2531                     'menclose',
2532                     'merror',
2533                     'mfenced',
2534                     'mfrac',
2535                     'mglyph',
2536                     'mi',
2537                     'mlabeledtr',
2538                     'mlongdiv',
2539                     'mmultiscripts',
2540                     'mn',
2541                     'mo',
2542                     'mover',
2543                     'mpadded',
2544                     'mphantom',
2545                     'mprescripts',
2546                     'mroot',
2547                     'mrow',
2548                     'ms',
2549                     'mscarries',
2550                     'mscarry',
2551                     'msgroup',
2552                     'msline',
2553                     'mspace',
2554                     'msqrt',
2555                     'msrow',
2556                     'mstack',
2557                     'mstyle',
2558                     'msub',
2559                     'msup',
2560                     'msubsup',
2561                     'mtable',
2562                     'mtd',
2563                     'mtext',
2564                     'mtr',
2565                     'munder',
2566                     'munderover']
2567
2568        return rv
2569
2570    def isNoneElement(self, obj):
2571        return self._getTag(obj) == 'none'
2572
2573    def isMathLayoutOnly(self, obj):
2574        return self._getTag(obj) in ['mrow', 'mstyle', 'merror', 'mpadded']
2575
2576    def isMathMultiline(self, obj):
2577        return self._getTag(obj) in ['mtable', 'mstack', 'mlongdiv']
2578
2579    def isMathEnclose(self, obj):
2580        return self._getTag(obj) == 'menclose'
2581
2582    def isMathFenced(self, obj):
2583        return self._getTag(obj) == 'mfenced'
2584
2585    def isMathFractionWithoutBar(self, obj):
2586        try:
2587            role = obj.getRole()
2588        except:
2589            msg = "ERROR: Exception getting role for %s" % obj
2590            debug.println(debug.LEVEL_INFO, msg, True)
2591
2592        if role != pyatspi.ROLE_MATH_FRACTION:
2593            return False
2594
2595        attrs = self.objectAttributes(obj)
2596        linethickness = attrs.get('linethickness')
2597        if not linethickness:
2598            return False
2599
2600        for char in linethickness:
2601            if char.isnumeric() and char != '0':
2602                return False
2603
2604        return True
2605
2606    def isMathPhantom(self, obj):
2607        return self._getTag(obj) == 'mphantom'
2608
2609    def isMathMultiScript(self, obj):
2610        return self._getTag(obj) == 'mmultiscripts'
2611
2612    def _isMathPrePostScriptSeparator(self, obj):
2613        return self._getTag(obj) == 'mprescripts'
2614
2615    def isMathSubOrSuperScript(self, obj):
2616        return self._getTag(obj) in ['msub', 'msup', 'msubsup']
2617
2618    def isMathTable(self, obj):
2619        return self._getTag(obj) == 'mtable'
2620
2621    def isMathTableRow(self, obj):
2622        return self._getTag(obj) in ['mtr', 'mlabeledtr']
2623
2624    def isMathTableCell(self, obj):
2625        return self._getTag(obj) == 'mtd'
2626
2627    def isMathUnderOrOverScript(self, obj):
2628        return self._getTag(obj) in ['mover', 'munder', 'munderover']
2629
2630    def _isMathSubElement(self, obj):
2631        return self._getTag(obj) == 'msub'
2632
2633    def _isMathSupElement(self, obj):
2634        return self._getTag(obj) == 'msup'
2635
2636    def _isMathSubsupElement(self, obj):
2637        return self._getTag(obj) == 'msubsup'
2638
2639    def _isMathUnderElement(self, obj):
2640        return self._getTag(obj) == 'munder'
2641
2642    def _isMathOverElement(self, obj):
2643        return self._getTag(obj) == 'mover'
2644
2645    def _isMathUnderOverElement(self, obj):
2646        return self._getTag(obj) == 'munderover'
2647
2648    def isMathSquareRoot(self, obj):
2649        return self._getTag(obj) == 'msqrt'
2650
2651    def isMathToken(self, obj):
2652        return self._getTag(obj) in ['mi', 'mn', 'mo', 'mtext', 'ms', 'mspace']
2653
2654    def isMathTopLevel(self, obj):
2655        return obj.getRole() == pyatspi.ROLE_MATH
2656
2657    def getMathAncestor(self, obj):
2658        if not self.isMath(obj):
2659            return None
2660
2661        if self.isMathTopLevel(obj):
2662            return obj
2663
2664        return pyatspi.findAncestor(obj, self.isMathTopLevel)
2665
2666    def getMathDenominator(self, obj):
2667        try:
2668            return obj[1]
2669        except:
2670            pass
2671
2672        return None
2673
2674    def getMathNumerator(self, obj):
2675        try:
2676            return obj[0]
2677        except:
2678            pass
2679
2680        return None
2681
2682    def getMathRootBase(self, obj):
2683        if self.isMathSquareRoot(obj):
2684            return obj
2685
2686        try:
2687            return obj[0]
2688        except:
2689            pass
2690
2691        return None
2692
2693    def getMathRootIndex(self, obj):
2694        try:
2695            return obj[1]
2696        except:
2697            pass
2698
2699        return None
2700
2701    def getMathScriptBase(self, obj):
2702        if self.isMathSubOrSuperScript(obj) \
2703           or self.isMathUnderOrOverScript(obj) \
2704           or self.isMathMultiScript(obj):
2705            return obj[0]
2706
2707        return None
2708
2709    def getMathScriptSubscript(self, obj):
2710        if self._isMathSubElement(obj) or self._isMathSubsupElement(obj):
2711            return obj[1]
2712
2713        return None
2714
2715    def getMathScriptSuperscript(self, obj):
2716        if self._isMathSupElement(obj):
2717            return obj[1]
2718
2719        if self._isMathSubsupElement(obj):
2720            return obj[2]
2721
2722        return None
2723
2724    def getMathScriptUnderscript(self, obj):
2725        if self._isMathUnderElement(obj) or self._isMathUnderOverElement(obj):
2726            return obj[1]
2727
2728        return None
2729
2730    def getMathScriptOverscript(self, obj):
2731        if self._isMathOverElement(obj):
2732            return obj[1]
2733
2734        if self._isMathUnderOverElement(obj):
2735            return obj[2]
2736
2737        return None
2738
2739    def _getMathPrePostScriptSeparator(self, obj):
2740        for child in obj:
2741            if self._isMathPrePostScriptSeparator(child):
2742                return child
2743
2744        return None
2745
2746    def getMathPrescripts(self, obj):
2747        separator = self._getMathPrePostScriptSeparator(obj)
2748        if not separator:
2749            return []
2750
2751        index = separator.getIndexInParent()
2752        return [obj[i] for i in range(index+1, obj.childCount)]
2753
2754    def getMathPostscripts(self, obj):
2755        separator = self._getMathPrePostScriptSeparator(obj)
2756        if separator:
2757            index = separator.getIndexInParent()
2758        else:
2759            index = obj.childCount
2760
2761        return [obj[i] for i in range(1, index)]
2762
2763    def getMathEnclosures(self, obj):
2764        if not self.isMathEnclose(obj):
2765            return []
2766
2767        attrs = self.objectAttributes(obj)
2768        return attrs.get('notation', 'longdiv').split()
2769
2770    def getMathFencedSeparators(self, obj):
2771        if not self.isMathFenced(obj):
2772            return ['']
2773
2774        attrs = self.objectAttributes(obj)
2775        return list(attrs.get('separators', ','))
2776
2777    def getMathFences(self, obj):
2778        if not self.isMathFenced(obj):
2779            return ['', '']
2780
2781        attrs = self.objectAttributes(obj)
2782        return [attrs.get('open', '('), attrs.get('close', ')')]
2783
2784    def getMathNestingLevel(self, obj, test=None):
2785        rv = self._mathNestingLevel.get(hash(obj))
2786        if rv is not None:
2787            return rv
2788
2789        if not test:
2790            test = lambda x: self._getTag(x) == self._getTag(obj)
2791
2792        rv = -1
2793        ancestor = obj
2794        while ancestor:
2795            ancestor = pyatspi.findAncestor(ancestor, test)
2796            rv += 1
2797
2798        self._mathNestingLevel[hash(obj)] = rv
2799        return rv
2800
2801    def filterContentsForPresentation(self, contents, inferLabels=False):
2802        def _include(x):
2803            obj, start, end, string = x
2804            if not obj or self.isDead(obj):
2805                return False
2806
2807            rv = self._shouldFilter.get(hash(obj))
2808            if rv is not None:
2809                return rv
2810
2811            displayedText = string or obj.name
2812            rv = True
2813            if ((self.isTextBlockElement(obj) or self.isLink(obj)) and not displayedText) \
2814               or (self.isContentEditableWithEmbeddedObjects(obj) and not string.strip()) \
2815               or self.isEmptyAnchor(obj) \
2816               or (self.hasNoSize(obj) and not displayedText) \
2817               or self.isHidden(obj) \
2818               or self.isOffScreenLabel(obj) \
2819               or self.isUselessImage(obj) \
2820               or self.isErrorForContents(obj, contents) \
2821               or self.isLabellingContents(obj, contents):
2822                rv = False
2823            elif obj.getRole() == pyatspi.ROLE_TABLE_ROW:
2824                rv = self.hasExplicitName(obj)
2825            else:
2826                widget = self.isInferredLabelForContents(x, contents)
2827                alwaysFilter = [pyatspi.ROLE_RADIO_BUTTON, pyatspi.ROLE_CHECK_BOX]
2828                if widget and (inferLabels or widget.getRole() in alwaysFilter):
2829                    rv = False
2830
2831            self._shouldFilter[hash(obj)] = rv
2832            return rv
2833
2834        if len(contents) == 1:
2835            return contents
2836
2837        rv = list(filter(_include, contents))
2838        self._shouldFilter = {}
2839        return rv
2840
2841    def needsSeparator(self, lastChar, nextChar):
2842        if lastChar.isspace() or nextChar.isspace():
2843            return False
2844
2845        openingPunctuation = ["(", "[", "{", "<"]
2846        closingPunctuation = [".", "?", "!", ":", ",", ";", ")", "]", "}", ">"]
2847        if lastChar in closingPunctuation or nextChar in openingPunctuation:
2848            return True
2849        if lastChar in openingPunctuation or nextChar in closingPunctuation:
2850            return False
2851
2852        return lastChar.isalnum()
2853
2854    def supportsSelectionAndTable(self, obj):
2855        interfaces = pyatspi.listInterfaces(obj)
2856        return 'Table' in interfaces and 'Selection' in interfaces
2857
2858    def isGridDescendant(self, obj):
2859        if not obj:
2860            return False
2861
2862        rv = self._isGridDescendant.get(hash(obj))
2863        if rv is not None:
2864            return rv
2865
2866        rv = pyatspi.findAncestor(obj, self.supportsSelectionAndTable) is not None
2867        self._isGridDescendant[hash(obj)] = rv
2868        return rv
2869
2870    def isSorted(self, obj):
2871        attrs = self.objectAttributes(obj, False)
2872        return not attrs.get("sort") in ("none", None)
2873
2874    def isAscending(self, obj):
2875        attrs = self.objectAttributes(obj, False)
2876        return attrs.get("sort") == "ascending"
2877
2878    def isDescending(self, obj):
2879        attrs = self.objectAttributes(obj, False)
2880        return attrs.get("sort") == "descending"
2881
2882    def _rowAndColumnIndices(self, obj):
2883        rowindex = colindex = None
2884
2885        attrs = self.objectAttributes(obj)
2886        rowindex = attrs.get('rowindex')
2887        colindex = attrs.get('colindex')
2888        if rowindex is not None and colindex is not None:
2889            return rowindex, colindex
2890
2891        isRow = lambda x: x and x.getRole() == pyatspi.ROLE_TABLE_ROW
2892        row = pyatspi.findAncestor(obj, isRow)
2893        if not row:
2894            return rowindex, colindex
2895
2896        attrs = self.objectAttributes(row)
2897        rowindex = attrs.get('rowindex', rowindex)
2898        colindex = attrs.get('colindex', colindex)
2899        return rowindex, colindex
2900
2901    def isCellWithNameFromHeader(self, obj):
2902        role = obj.getRole()
2903        if role != pyatspi.ROLE_TABLE_CELL:
2904            return False
2905
2906        header = self.columnHeaderForCell(obj)
2907        if header and header.name and header.name == obj.name:
2908            return True
2909
2910        header = self.rowHeaderForCell(obj)
2911        if header and header.name and header.name == obj.name:
2912            return True
2913
2914        return False
2915
2916    def labelForCellCoordinates(self, obj):
2917        attrs = self.objectAttributes(obj)
2918
2919        # The ARIA feature is still in the process of being discussed.
2920        collabel = attrs.get('colindextext', attrs.get('coltext'))
2921        rowlabel = attrs.get('rowindextext', attrs.get('rowtext'))
2922        if collabel is not None and rowlabel is not None:
2923            return '%s%s' % (collabel, rowlabel)
2924
2925        isRow = lambda x: x and x.getRole() == pyatspi.ROLE_TABLE_ROW
2926        row = pyatspi.findAncestor(obj, isRow)
2927        if not row:
2928            return ''
2929
2930        attrs = self.objectAttributes(row)
2931        collabel = attrs.get('colindextext', attrs.get('coltext', collabel))
2932        rowlabel = attrs.get('rowindextext', attrs.get('rowtext', rowlabel))
2933        if collabel is not None and rowlabel is not None:
2934            return '%s%s' % (collabel, rowlabel)
2935
2936        return ''
2937
2938    def coordinatesForCell(self, obj, preferAttribute=True):
2939        roles = [pyatspi.ROLE_TABLE_CELL,
2940                 pyatspi.ROLE_TABLE_COLUMN_HEADER,
2941                 pyatspi.ROLE_TABLE_ROW_HEADER,
2942                 pyatspi.ROLE_COLUMN_HEADER,
2943                 pyatspi.ROLE_ROW_HEADER]
2944        if not (obj and obj.getRole() in roles):
2945            return -1, -1
2946
2947        if preferAttribute:
2948            rowindex, colindex = self._rowAndColumnIndices(obj)
2949            if rowindex is not None and colindex is not None:
2950                return int(rowindex) - 1, int(colindex) - 1
2951
2952        return super().coordinatesForCell(obj, preferAttribute)
2953
2954    def rowAndColumnCount(self, obj, preferAttribute=True):
2955        rows, cols = super().rowAndColumnCount(obj)
2956        if not preferAttribute:
2957            return rows, cols
2958
2959        attrs = self.objectAttributes(obj)
2960        rows = attrs.get('rowcount', rows)
2961        cols = attrs.get('colcount', cols)
2962        return int(rows), int(cols)
2963
2964    def shouldReadFullRow(self, obj):
2965        if not (obj and self.inDocumentContent(obj)):
2966            return super().shouldReadFullRow(obj)
2967
2968        if not super().shouldReadFullRow(obj):
2969            return False
2970
2971        if self.isGridDescendant(obj):
2972            return not self._script.inFocusMode()
2973
2974        if self.lastInputEventWasLineNav():
2975            return False
2976
2977        if self.lastInputEventWasMouseButton():
2978            return False
2979
2980        return True
2981
2982    def isEntryDescendant(self, obj):
2983        if not obj:
2984            return False
2985
2986        rv = self._isEntryDescendant.get(hash(obj))
2987        if rv is not None:
2988            return rv
2989
2990        isEntry = lambda x: x and x.getRole() == pyatspi.ROLE_ENTRY
2991        rv = pyatspi.findAncestor(obj, isEntry) is not None
2992        self._isEntryDescendant[hash(obj)] = rv
2993        return rv
2994
2995    def isLabelDescendant(self, obj):
2996        if not obj:
2997            return False
2998
2999        rv = self._isLabelDescendant.get(hash(obj))
3000        if rv is not None:
3001            return rv
3002
3003        isLabel = lambda x: x and x.getRole() in [pyatspi.ROLE_LABEL, pyatspi.ROLE_CAPTION]
3004        rv = pyatspi.findAncestor(obj, isLabel) is not None
3005        self._isLabelDescendant[hash(obj)] = rv
3006        return rv
3007
3008    def isMenuInCollapsedSelectElement(self, obj):
3009        return False
3010
3011    def isMenuDescendant(self, obj):
3012        if not obj:
3013            return False
3014
3015        rv = self._isMenuDescendant.get(hash(obj))
3016        if rv is not None:
3017            return rv
3018
3019        isMenu = lambda x: x and x.getRole() == pyatspi.ROLE_MENU
3020        rv = pyatspi.findAncestor(obj, isMenu) is not None
3021        self._isMenuDescendant[hash(obj)] = rv
3022        return rv
3023
3024    def isNavigableToolTipDescendant(self, obj):
3025        if not obj:
3026            return False
3027
3028        rv = self._isNavigableToolTipDescendant.get(hash(obj))
3029        if rv is not None:
3030            return rv
3031
3032        isToolTip = lambda x: x and x.getRole() == pyatspi.ROLE_TOOL_TIP
3033        if isToolTip(obj):
3034            ancestor = obj
3035        else:
3036            ancestor = pyatspi.findAncestor(obj, isToolTip)
3037        rv = ancestor and not self.isNonNavigablePopup(ancestor)
3038        self._isNavigableToolTipDescendant[hash(obj)] = rv
3039        return rv
3040
3041    def isToolBarDescendant(self, obj):
3042        if not obj:
3043            return False
3044
3045        rv = self._isToolBarDescendant.get(hash(obj))
3046        if rv is not None:
3047            return rv
3048
3049        isToolBar = lambda x: x and x.getRole() == pyatspi.ROLE_TOOL_BAR
3050        rv = pyatspi.findAncestor(obj, isToolBar) is not None
3051        self._isToolBarDescendant[hash(obj)] = rv
3052        return rv
3053
3054    def isWebAppDescendant(self, obj):
3055        if not obj:
3056            return False
3057
3058        rv = self._isWebAppDescendant.get(hash(obj))
3059        if rv is not None:
3060            return rv
3061
3062        isEmbedded = lambda x: x and x.getRole() == pyatspi.ROLE_EMBEDDED
3063        rv = pyatspi.findAncestor(obj, isEmbedded) is not None
3064        self._isWebAppDescendant[hash(obj)] = rv
3065        return rv
3066
3067    def isLayoutOnly(self, obj):
3068        if not (obj and self.inDocumentContent(obj)):
3069            return super().isLayoutOnly(obj)
3070
3071        rv = self._isLayoutOnly.get(hash(obj))
3072        if rv is not None:
3073            if rv:
3074                msg = "WEB: %s is deemed to be layout only" % obj
3075                debug.println(debug.LEVEL_INFO, msg, True)
3076            return rv
3077
3078        try:
3079            role = obj.getRole()
3080            state = obj.getState()
3081        except:
3082            msg = "ERROR: Exception getting role and state for %s" % obj
3083            debug.println(debug.LEVEL_INFO, msg, True)
3084            return False
3085
3086        if role == pyatspi.ROLE_LIST:
3087            rv = self.treatAsDiv(obj)
3088        elif self.isMath(obj):
3089            rv = False
3090        elif self.isLandmark(obj):
3091            rv = False
3092        elif self.isContentDeletion(obj):
3093            rv = False
3094        elif self.isContentInsertion(obj):
3095            rv = False
3096        elif self.isContentMarked(obj):
3097            rv = False
3098        elif self.isContentSuggestion(obj):
3099            rv = False
3100        elif self.isDPub(obj):
3101            rv = False
3102        elif self.isFeed(obj):
3103            rv = False
3104        elif self.isFigure(obj):
3105            rv = False
3106        elif self.isGrid(obj):
3107            rv = False
3108        elif role in [pyatspi.ROLE_COLUMN_HEADER, pyatspi.ROLE_ROW_HEADER]:
3109            rv = False
3110        elif role == pyatspi.ROLE_SEPARATOR:
3111            rv = False
3112        elif role == pyatspi.ROLE_PANEL:
3113            rv = not self.hasExplicitName(obj)
3114        elif role == pyatspi.ROLE_TABLE_ROW and not state.contains(pyatspi.STATE_EXPANDABLE):
3115            rv = not self.hasExplicitName(obj)
3116        elif self.isCustomImage(obj):
3117            rv = False
3118        else:
3119            rv = super().isLayoutOnly(obj)
3120
3121        if rv:
3122            msg = "WEB: %s is deemed to be layout only" % obj
3123            debug.println(debug.LEVEL_INFO, msg, True)
3124
3125        self._isLayoutOnly[hash(obj)] = rv
3126        return rv
3127
3128    def elementIsPreformattedText(self, obj):
3129        if self._getTag(obj) in ["pre", "code"]:
3130            return True
3131
3132        if "code" in self._getXMLRoles(obj):
3133            return True
3134
3135        return False
3136
3137    def elementLinesAreSingleWords(self, obj):
3138        if not (obj and self.inDocumentContent(obj)):
3139            return False
3140
3141        if self.elementIsPreformattedText(obj):
3142            return False
3143
3144        rv = self._elementLinesAreSingleWords.get(hash(obj))
3145        if rv is not None:
3146            return rv
3147
3148        text = self.queryNonEmptyText(obj)
3149        if not text:
3150            return False
3151
3152        try:
3153            nChars = text.characterCount
3154        except:
3155            return False
3156
3157        if not nChars:
3158            return False
3159
3160        # If we have a series of embedded object characters, there's a reasonable chance
3161        # they'll look like the one-word-per-line CSSified text we're trying to detect.
3162        # We don't want that false positive. By the same token, the one-word-per-line
3163        # CSSified text we're trying to detect can have embedded object characters. So
3164        # if we have more than 30% EOCs, don't use this workaround. (The 30% is based on
3165        # testing with problematic text.)
3166        eocs = re.findall(self.EMBEDDED_OBJECT_CHARACTER, text.getText(0, -1))
3167        if len(eocs)/nChars > 0.3:
3168            return False
3169
3170        try:
3171            obj.clearCache()
3172            state = obj.getState()
3173        except:
3174            msg = "ERROR: Exception getting state for %s" % obj
3175            debug.println(debug.LEVEL_INFO, msg, True)
3176            return False
3177
3178        tokens = list(filter(lambda x: x, re.split(r"[\s\ufffc]", text.getText(0, -1))))
3179
3180        # Note: We cannot check for the editable-text interface, because Gecko
3181        # seems to be exposing that for non-editable things. Thanks Gecko.
3182        rv = not state.contains(pyatspi.STATE_EDITABLE) and len(tokens) > 1
3183        if rv:
3184            boundary = pyatspi.TEXT_BOUNDARY_LINE_START
3185            i = 0
3186            while i < nChars:
3187                string, start, end = text.getTextAtOffset(i, boundary)
3188                if len(string.split()) != 1:
3189                    rv = False
3190                    break
3191                i = max(i+1, end)
3192
3193        self._elementLinesAreSingleWords[hash(obj)] = rv
3194        return rv
3195
3196    def elementLinesAreSingleChars(self, obj):
3197        if not (obj and self.inDocumentContent(obj)):
3198            return False
3199
3200        rv = self._elementLinesAreSingleChars.get(hash(obj))
3201        if rv is not None:
3202            return rv
3203
3204        text = self.queryNonEmptyText(obj)
3205        if not text:
3206            return False
3207
3208        try:
3209            nChars = text.characterCount
3210        except:
3211            return False
3212
3213        if not nChars:
3214            return False
3215
3216        # If we have a series of embedded object characters, there's a reasonable chance
3217        # they'll look like the one-char-per-line CSSified text we're trying to detect.
3218        # We don't want that false positive. By the same token, the one-char-per-line
3219        # CSSified text we're trying to detect can have embedded object characters. So
3220        # if we have more than 30% EOCs, don't use this workaround. (The 30% is based on
3221        # testing with problematic text.)
3222        eocs = re.findall(self.EMBEDDED_OBJECT_CHARACTER, text.getText(0, -1))
3223        if len(eocs)/nChars > 0.3:
3224            return False
3225
3226        try:
3227            obj.clearCache()
3228            state = obj.getState()
3229        except:
3230            msg = "ERROR: Exception getting state for %s" % obj
3231            debug.println(debug.LEVEL_INFO, msg, True)
3232            return False
3233
3234        # Note: We cannot check for the editable-text interface, because Gecko
3235        # seems to be exposing that for non-editable things. Thanks Gecko.
3236        rv = not state.contains(pyatspi.STATE_EDITABLE)
3237        if rv:
3238            boundary = pyatspi.TEXT_BOUNDARY_LINE_START
3239            for i in range(nChars):
3240                char = text.getText(i, i + 1)
3241                if char.isspace() or char in ["\ufffc", "\ufffd"]:
3242                    continue
3243
3244                string, start, end = text.getTextAtOffset(i, boundary)
3245                if len(string.strip()) > 1:
3246                    rv = False
3247                    break
3248
3249        self._elementLinesAreSingleChars[hash(obj)] = rv
3250        return rv
3251
3252    def isOffScreenLabel(self, obj):
3253        if not (obj and self.inDocumentContent(obj)):
3254            return False
3255
3256        rv = self._isOffScreenLabel.get(hash(obj))
3257        if rv is not None:
3258            return rv
3259
3260        rv = False
3261        targets = self.labelTargets(obj)
3262        if targets:
3263            try:
3264                text = obj.queryText()
3265                end = text.characterCount
3266            except:
3267                end = 1
3268            x, y, width, height = self.getExtents(obj, 0, end)
3269            if x < 0 or y < 0:
3270                rv = True
3271
3272        self._isOffScreenLabel[hash(obj)] = rv
3273        return rv
3274
3275    def isDetachedDocument(self, obj):
3276        docRoles = [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_DOCUMENT_WEB]
3277        if (obj and obj.getRole() in docRoles):
3278            if obj.parent is None or self.isZombie(obj.parent):
3279                msg = "WEB: %s is a detached document" % obj
3280                debug.println(debug.LEVEL_INFO, msg, True)
3281                return True
3282
3283        return False
3284
3285    def iframeForDetachedDocument(self, obj, root=None):
3286        root = root or self.documentFrame()
3287        isIframe = lambda x: x and x.getRole() == pyatspi.ROLE_INTERNAL_FRAME
3288        iframes = self.findAllDescendants(root, isIframe)
3289        for iframe in iframes:
3290            if obj in iframe:
3291                # We won't change behavior, but we do want to log all bogosity.
3292                self._isBrokenChildParentTree(obj, iframe)
3293
3294                msg = "WEB: Returning %s as iframe parent of detached %s" % (iframe, obj)
3295                debug.println(debug.LEVEL_INFO, msg, True)
3296                return iframe
3297
3298        return None
3299
3300    def _objectBoundsMightBeBogus(self, obj):
3301        if not (obj and self.inDocumentContent(obj)):
3302            return super()._objectBoundsMightBeBogus(obj)
3303
3304        if obj.getRole() != pyatspi.ROLE_LINK or "Text" not in pyatspi.listInterfaces(obj):
3305            return False
3306
3307        text = obj.queryText()
3308        start = list(text.getRangeExtents(0, 1, 0))
3309        end = list(text.getRangeExtents(text.characterCount - 1, text.characterCount, 0))
3310        if self.extentsAreOnSameLine(start, end):
3311            return False
3312
3313        if not self.hasPresentableText(obj.parent):
3314            return False
3315
3316        msg = "WEB: Objects bounds of %s might be bogus" % obj
3317        debug.println(debug.LEVEL_INFO, msg, True)
3318        return True
3319
3320    def _isBrokenChildParentTree(self, child, parent):
3321        if not (child and parent):
3322            return False
3323
3324        try:
3325            childIsChildOfParent = child in parent
3326        except:
3327            msg = "WEB: Exception checking if %s is in %s" % (child, parent)
3328            debug.println(debug.LEVEL_INFO, msg, True)
3329            childIsChildOfParent = False
3330        else:
3331            msg = "WEB: %s is child of %s: %s" % (child, parent, childIsChildOfParent)
3332            debug.println(debug.LEVEL_INFO, msg, True)
3333
3334        try:
3335            parentIsParentOfChild = child.parent == parent
3336        except:
3337            msg = "WEB: Exception getting parent of %s" % child
3338            debug.println(debug.LEVEL_INFO, msg, True)
3339            parentIsParentOfChild = False
3340        else:
3341            msg = "WEB: %s is parent of %s: %s" % (parent, child, parentIsParentOfChild)
3342            debug.println(debug.LEVEL_INFO, msg, True)
3343
3344        if parentIsParentOfChild != childIsChildOfParent:
3345            msg = "FAIL: The above is broken and likely needs to be fixed by the toolkit."
3346            debug.println(debug.LEVEL_INFO, msg, True)
3347            return True
3348
3349        return False
3350
3351    def targetsForLabel(self, obj):
3352        isLabel = lambda r: r.getRelationType() == pyatspi.RELATION_LABEL_FOR
3353        try:
3354            relations = list(filter(isLabel, obj.getRelationSet()))
3355        except:
3356            msg = "WEB: Exception getting relations of %s" % obj
3357            debug.println(debug.LEVEL_INFO, msg, True)
3358            return []
3359
3360        if not relations:
3361            return []
3362
3363        r = relations[0]
3364        rv = set([r.getTarget(i) for i in range(r.getNTargets())])
3365
3366        if obj in rv:
3367            msg = 'WARNING: %s claims to be a label for itself' % obj
3368            debug.println(debug.LEVEL_INFO, msg, True)
3369            rv.remove(obj)
3370
3371        return list(filter(lambda x: x is not None, rv))
3372
3373    def labelTargets(self, obj):
3374        if not (obj and self.inDocumentContent(obj)):
3375            return []
3376
3377        rv = self._labelTargets.get(hash(obj))
3378        if rv is not None:
3379            return rv
3380
3381        rv = [hash(t) for t in self.targetsForLabel(obj)]
3382        self._labelTargets[hash(obj)] = rv
3383        return rv
3384
3385    def isLinkAncestorOfImageInContents(self, link, contents):
3386        if not self.isLink(link):
3387            return False
3388
3389        for obj, start, end, string in contents:
3390            if obj.getRole() != pyatspi.ROLE_IMAGE:
3391                continue
3392            if pyatspi.findAncestor(obj, lambda x: x == link):
3393                return True
3394
3395        return False
3396
3397    def isInferredLabelForContents(self, content, contents):
3398        obj, start, end, string = content
3399        objs = list(filter(self.shouldInferLabelFor, [x[0] for x in contents]))
3400        if not objs:
3401            return None
3402
3403        for o in objs:
3404            label, sources = self.inferLabelFor(o)
3405            if obj in sources and label.strip() == string.strip():
3406                return o
3407
3408        return None
3409
3410    def isLabellingInteractiveElement(self, obj):
3411        if self._labelTargets.get(hash(obj)) == []:
3412            return False
3413
3414        targets = self.targetsForLabel(obj)
3415        for target in targets:
3416            if target.getState().contains(pyatspi.STATE_FOCUSABLE):
3417                return True
3418
3419        return False
3420
3421    def isLabellingContents(self, obj, contents=[]):
3422        if self.isFocusModeWidget(obj):
3423            return False
3424
3425        targets = self.labelTargets(obj)
3426        if not contents:
3427            return bool(targets) or self.isLabelDescendant(obj)
3428
3429        for acc, start, end, string in contents:
3430            if hash(acc) in targets:
3431                return True
3432
3433        if not self.isTextBlockElement(obj):
3434            return False
3435
3436        if not self.isLabelDescendant(obj):
3437            return False
3438
3439        for acc, start, end, string in contents:
3440            if not self.isLabelDescendant(acc) or self.isTextBlockElement(acc):
3441                continue
3442
3443            ancestor = self.commonAncestor(acc, obj)
3444            if ancestor and ancestor.getRole() in [pyatspi.ROLE_LABEL, pyatspi.ROLE_CAPTION]:
3445                return True
3446
3447        return False
3448
3449    def isAnchor(self, obj):
3450        if not (obj and self.inDocumentContent(obj)):
3451            return False
3452
3453        rv = self._isAnchor.get(hash(obj))
3454        if rv is not None:
3455            return rv
3456
3457        rv = False
3458        if obj.getRole() == pyatspi.ROLE_LINK \
3459           and not obj.getState().contains(pyatspi.STATE_FOCUSABLE) \
3460           and not 'jump' in self._getActionNames(obj) \
3461           and not self._getXMLRoles(obj):
3462            rv = True
3463
3464        self._isAnchor[hash(obj)] = rv
3465        return rv
3466
3467    def isEmptyAnchor(self, obj):
3468        if not self.isAnchor(obj):
3469            return False
3470
3471        return self.queryNonEmptyText(obj) is None
3472
3473    def isEmptyToolTip(self, obj):
3474        return obj and obj.getRole() == pyatspi.ROLE_TOOL_TIP \
3475            and self.queryNonEmptyText(obj) is None
3476
3477    def isBrowserUIAlert(self, obj):
3478        if not (obj and obj.getRole() == pyatspi.ROLE_ALERT):
3479            return False
3480
3481        if self.inDocumentContent(obj):
3482            return False
3483
3484        return True
3485
3486    def isTopLevelBrowserUIAlert(self, obj):
3487        if not self.isBrowserUIAlert(obj):
3488            return False
3489
3490        parent = obj.parent
3491        while parent and self.isLayoutOnly(parent):
3492            parent = parent.parent
3493
3494        return parent.getRole() == pyatspi.ROLE_FRAME
3495
3496    def _getActionNames(self, obj):
3497        try:
3498            action = obj.queryAction()
3499            names = [action.getName(i).lower() for i in range(action.nActions)]
3500        except NotImplementedError:
3501            return []
3502        except:
3503            msg = "WEB: Exception getting actions for %s" % obj
3504            debug.println(debug.LEVEL_INFO, msg, True)
3505            return []
3506
3507        return list(filter(lambda x: x, names))
3508
3509    def isClickableElement(self, obj):
3510        if not (obj and self.inDocumentContent(obj)):
3511            return False
3512
3513        rv = self._isClickableElement.get(hash(obj))
3514        if rv is not None:
3515            return rv
3516
3517        rv = False
3518        if not obj.getState().contains(pyatspi.STATE_FOCUSABLE) \
3519           and not self.isFocusModeWidget(obj):
3520            names = self._getActionNames(obj)
3521            rv = "click" in names
3522
3523        if rv and not obj.name and "Text" in pyatspi.listInterfaces(obj):
3524            string = obj.queryText().getText(0, -1)
3525            if not string.strip():
3526                rv = obj.getRole() not in [pyatspi.ROLE_STATIC, pyatspi.ROLE_LINK]
3527
3528        self._isClickableElement[hash(obj)] = rv
3529        return rv
3530
3531    def isCodeDescendant(self, obj):
3532        if not (obj and self.inDocumentContent(obj)):
3533            return super().isCodeDescendant(obj)
3534
3535        rv = self._isCodeDescendant.get(hash(obj))
3536        if rv is not None:
3537            return rv
3538
3539        rv = pyatspi.findAncestor(obj, self.isCode) is not None
3540        self._isCodeDescendant[hash(obj)] = rv
3541        return rv
3542
3543    def isCode(self, obj):
3544        if not (obj and self.inDocumentContent(obj)):
3545            return super().isCode(obj)
3546
3547        return self._getTag(obj) == "code" or "code" in self._getXMLRoles(obj)
3548
3549    def getComboBoxValue(self, obj):
3550        attrs = self.objectAttributes(obj, False)
3551        return attrs.get("valuetext", super().getComboBoxValue(obj))
3552
3553    def isEditableComboBox(self, obj):
3554        if not (obj and self.inDocumentContent(obj)):
3555            return super().isEditableComboBox(obj)
3556
3557        rv = self._isEditableComboBox.get(hash(obj))
3558        if rv is not None:
3559            return rv
3560
3561        try:
3562            role = obj.getRole()
3563            state = obj.getState()
3564        except:
3565            msg = "ERROR: Exception getting role and state for %s" % obj
3566            debug.println(debug.LEVEL_INFO, msg, True)
3567            return False
3568
3569        rv = False
3570        if role == pyatspi.ROLE_COMBO_BOX:
3571            rv = state.contains(pyatspi.STATE_EDITABLE)
3572
3573        self._isEditableComboBox[hash(obj)] = rv
3574        return rv
3575
3576    def isDPub(self, obj):
3577        if not (obj and self.inDocumentContent(obj)):
3578            return False
3579
3580        roles = self._getXMLRoles(obj)
3581        rv = bool(list(filter(lambda x: x.startswith("doc-"), roles)))
3582        return rv
3583
3584    def isDPubAbstract(self, obj):
3585        return 'doc-abstract' in self._getXMLRoles(obj)
3586
3587    def isDPubAcknowledgments(self, obj):
3588        return 'doc-acknowledgments' in self._getXMLRoles(obj)
3589
3590    def isDPubAfterword(self, obj):
3591        return 'doc-afterword' in self._getXMLRoles(obj)
3592
3593    def isDPubAppendix(self, obj):
3594        return 'doc-appendix' in self._getXMLRoles(obj)
3595
3596    def isDPubBacklink(self, obj):
3597        return 'doc-backlink' in self._getXMLRoles(obj)
3598
3599    def isDPubBiblioref(self, obj):
3600        return 'doc-biblioref' in self._getXMLRoles(obj)
3601
3602    def isDPubBibliography(self, obj):
3603        return 'doc-bibliography' in self._getXMLRoles(obj)
3604
3605    def isDPubChapter(self, obj):
3606        return 'doc-chapter' in self._getXMLRoles(obj)
3607
3608    def isDPubColophon(self, obj):
3609        return 'doc-colophon' in self._getXMLRoles(obj)
3610
3611    def isDPubConclusion(self, obj):
3612        return 'doc-conclusion' in self._getXMLRoles(obj)
3613
3614    def isDPubCover(self, obj):
3615        return 'doc-cover' in self._getXMLRoles(obj)
3616
3617    def isDPubCredit(self, obj):
3618        return 'doc-credit' in self._getXMLRoles(obj)
3619
3620    def isDPubCredits(self, obj):
3621        return 'doc-credits' in self._getXMLRoles(obj)
3622
3623    def isDPubDedication(self, obj):
3624        return 'doc-dedication' in self._getXMLRoles(obj)
3625
3626    def isDPubEndnote(self, obj):
3627        return 'doc-endnote' in self._getXMLRoles(obj)
3628
3629    def isDPubEndnotes(self, obj):
3630        return 'doc-endnotes' in self._getXMLRoles(obj)
3631
3632    def isDPubEpigraph(self, obj):
3633        return 'doc-epigraph' in self._getXMLRoles(obj)
3634
3635    def isDPubEpilogue(self, obj):
3636        return 'doc-epilogue' in self._getXMLRoles(obj)
3637
3638    def isDPubErrata(self, obj):
3639        return 'doc-errata' in self._getXMLRoles(obj)
3640
3641    def isDPubExample(self, obj):
3642        return 'doc-example' in self._getXMLRoles(obj)
3643
3644    def isDPubFootnote(self, obj):
3645        return 'doc-footnote' in self._getXMLRoles(obj)
3646
3647    def isDPubForeword(self, obj):
3648        return 'doc-foreword' in self._getXMLRoles(obj)
3649
3650    def isDPubGlossary(self, obj):
3651        return 'doc-glossary' in self._getXMLRoles(obj)
3652
3653    def isDPubGlossref(self, obj):
3654        return 'doc-glossref' in self._getXMLRoles(obj)
3655
3656    def isDPubIndex(self, obj):
3657        return 'doc-index' in self._getXMLRoles(obj)
3658
3659    def isDPubIntroduction(self, obj):
3660        return 'doc-introduction' in self._getXMLRoles(obj)
3661
3662    def isDPubNoteref(self, obj):
3663        return 'doc-noteref' in self._getXMLRoles(obj)
3664
3665    def isDPubPagelist(self, obj):
3666        return 'doc-pagelist' in self._getXMLRoles(obj)
3667
3668    def isDPubPagebreak(self, obj):
3669        return 'doc-pagebreak' in self._getXMLRoles(obj)
3670
3671    def isDPubPart(self, obj):
3672        return 'doc-part' in self._getXMLRoles(obj)
3673
3674    def isDPubPreface(self, obj):
3675        return 'doc-preface' in self._getXMLRoles(obj)
3676
3677    def isDPubPrologue(self, obj):
3678        return 'doc-prologue' in self._getXMLRoles(obj)
3679
3680    def isDPubPullquote(self, obj):
3681        return 'doc-pullquote' in self._getXMLRoles(obj)
3682
3683    def isDPubQna(self, obj):
3684        return 'doc-qna' in self._getXMLRoles(obj)
3685
3686    def isDPubSubtitle(self, obj):
3687        return 'doc-subtitle' in self._getXMLRoles(obj)
3688
3689    def isDPubToc(self, obj):
3690        return 'doc-toc' in self._getXMLRoles(obj)
3691
3692    def isErrorMessage(self, obj):
3693        if not (obj and self.inDocumentContent(obj)):
3694            return super().isErrorMessage(obj)
3695
3696        rv = self._isErrorMessage.get(hash(obj))
3697        if rv is not None:
3698            return rv
3699
3700        try:
3701            relations = obj.getRelationSet()
3702        except:
3703            msg = 'ERROR: Exception getting relationset for %s' % obj
3704            debug.println(debug.LEVEL_INFO, msg, True)
3705            return False
3706
3707        # Remove this when we bump dependencies to 2.26
3708        try:
3709            relationType = pyatspi.RELATION_ERROR_FOR
3710        except:
3711            rv = False
3712        else:
3713            isMessage = lambda r: r.getRelationType() == relationType
3714            rv = bool(list(filter(isMessage, relations)))
3715
3716        self._isErrorMessage[hash(obj)] = rv
3717        return rv
3718
3719    def isFakePlaceholderForEntry(self, obj):
3720        if not (obj and self.inDocumentContent(obj) and obj.parent):
3721            return False
3722
3723        entry = pyatspi.findAncestor(obj, lambda x: x and x.getRole() == pyatspi.ROLE_ENTRY)
3724        if not (entry and entry.name):
3725            return False
3726
3727        def _isMatch(x):
3728            try:
3729                role = x.getRole()
3730                string = x.queryText().getText(0, -1).strip()
3731            except:
3732                return False
3733            return role in [pyatspi.ROLE_SECTION, pyatspi.ROLE_STATIC] and entry.name == string
3734
3735        if _isMatch(obj):
3736            return True
3737
3738        return pyatspi.findDescendant(obj, _isMatch) is not None
3739
3740    def isGrid(self, obj):
3741        return 'grid' in self._getXMLRoles(obj)
3742
3743    def isGridCell(self, obj):
3744        return 'gridcell' in self._getXMLRoles(obj)
3745
3746    def isInlineListItem(self, obj):
3747        if not (obj and self.inDocumentContent(obj)):
3748            return False
3749
3750        rv = self._isInlineListItem.get(hash(obj))
3751        if rv is not None:
3752            return rv
3753
3754        if obj.getRole() != pyatspi.ROLE_LIST_ITEM:
3755            rv = False
3756        else:
3757            displayStyle = self._getDisplayStyle(obj)
3758            rv = displayStyle and "inline" in displayStyle
3759
3760        self._isInlineListItem[hash(obj)] = rv
3761        return rv
3762
3763    def isBlockListDescendant(self, obj):
3764        if not self.isListDescendant(obj):
3765            return False
3766
3767        return not self.isInlineListDescendant(obj)
3768
3769    def isListDescendant(self, obj):
3770        if not (obj and self.inDocumentContent(obj)):
3771            return False
3772
3773        rv = self._isListDescendant.get(hash(obj))
3774        if rv is not None:
3775            return rv
3776
3777        isList = lambda x: x and x.getRole() == pyatspi.ROLE_LIST
3778        ancestor = pyatspi.findAncestor(obj, isList)
3779        rv = ancestor is not None
3780
3781        self._isListDescendant[hash(obj)] = rv
3782        return rv
3783
3784    def isInlineListDescendant(self, obj):
3785        if not (obj and self.inDocumentContent(obj)):
3786            return False
3787
3788        rv = self._isInlineListDescendant.get(hash(obj))
3789        if rv is not None:
3790            return rv
3791
3792        if self.isInlineListItem(obj):
3793            rv = True
3794        else:
3795            ancestor = pyatspi.findAncestor(obj, self.isInlineListItem)
3796            rv = ancestor is not None
3797
3798        self._isInlineListDescendant[hash(obj)] = rv
3799        return rv
3800
3801    def listForInlineListDescendant(self, obj):
3802        if not self.isInlineListDescendant(obj):
3803            return None
3804
3805        isList = lambda x: x and x.getRole() == pyatspi.ROLE_LIST
3806        return pyatspi.findAncestor(obj, isList)
3807
3808    def isFeed(self, obj):
3809        return 'feed' in self._getXMLRoles(obj)
3810
3811    def isFigure(self, obj):
3812        return 'figure' in self._getXMLRoles(obj) or self._getTag(obj) == 'figure'
3813
3814    def isLandmark(self, obj):
3815        if not (obj and self.inDocumentContent(obj)):
3816            return False
3817
3818        rv = self._isLandmark.get(hash(obj))
3819        if rv is not None:
3820            return rv
3821
3822        if obj.getRole() == pyatspi.ROLE_LANDMARK:
3823            rv = True
3824        elif self.isLandmarkRegion(obj):
3825            rv = bool(obj.name)
3826        else:
3827            roles = self._getXMLRoles(obj)
3828            rv = bool(list(filter(lambda x: x in self.getLandmarkTypes(), roles)))
3829
3830        self._isLandmark[hash(obj)] = rv
3831        return rv
3832
3833    def isLandmarkWithoutType(self, obj):
3834        roles = self._getXMLRoles(obj)
3835        return not roles
3836
3837    def isLandmarkBanner(self, obj):
3838        return 'banner' in self._getXMLRoles(obj)
3839
3840    def isLandmarkComplementary(self, obj):
3841        return 'complementary' in self._getXMLRoles(obj)
3842
3843    def isLandmarkContentInfo(self, obj):
3844        return 'contentinfo' in self._getXMLRoles(obj)
3845
3846    def isLandmarkForm(self, obj):
3847        return 'form' in self._getXMLRoles(obj)
3848
3849    def isLandmarkMain(self, obj):
3850        return 'main' in self._getXMLRoles(obj)
3851
3852    def isLandmarkNavigation(self, obj):
3853        return 'navigation' in self._getXMLRoles(obj)
3854
3855    def isLandmarkRegion(self, obj):
3856        return 'region' in self._getXMLRoles(obj)
3857
3858    def isLandmarkSearch(self, obj):
3859        return 'search' in self._getXMLRoles(obj)
3860
3861    def isLiveRegion(self, obj):
3862        if not (obj and self.inDocumentContent(obj)):
3863            return False
3864
3865        attrs = self.objectAttributes(obj)
3866        return 'container-live' in attrs
3867
3868    def isLink(self, obj):
3869        if not obj:
3870            return False
3871
3872        rv = self._isLink.get(hash(obj))
3873        if rv is not None:
3874            return rv
3875
3876        role = obj.getRole()
3877        if role == pyatspi.ROLE_LINK and not self.isAnchor(obj):
3878            rv = True
3879        elif role == pyatspi.ROLE_STATIC \
3880           and obj.parent.getRole() == pyatspi.ROLE_LINK \
3881           and obj.name and obj.name == obj.parent.name:
3882            rv = True
3883        else:
3884            rv = False
3885
3886        self._isLink[hash(obj)] = rv
3887        return rv
3888
3889    def isNonNavigablePopup(self, obj):
3890        if not (obj and self.inDocumentContent(obj)):
3891            return False
3892
3893        rv = self._isNonNavigablePopup.get(hash(obj))
3894        if rv is not None:
3895            return rv
3896
3897        rv = obj.getRole() == pyatspi.ROLE_TOOL_TIP \
3898            and not obj.getState().contains(pyatspi.STATE_FOCUSABLE)
3899
3900        self._isNonNavigablePopup[hash(obj)] = rv
3901        return rv
3902
3903    def hasUselessCanvasDescendant(self, obj):
3904        if not (obj and self.inDocumentContent(obj)):
3905            return False
3906
3907        rv = self._hasUselessCanvasDescendant.get(hash(obj))
3908        if rv is not None:
3909            return rv
3910
3911        isCanvas = lambda x: x and x.getRole() == pyatspi.ROLE_CANVAS
3912        canvases = self.findAllDescendants(obj, isCanvas)
3913        rv = len(list(filter(self.isUselessImage, canvases))) > 0
3914
3915        self._hasUselessCanvasDescendant[hash(obj)] = rv
3916        return rv
3917
3918    def isTextSubscriptOrSuperscript(self, obj):
3919        if self.isMath(obj):
3920            return False
3921
3922        return obj.getRole() in [pyatspi.ROLE_SUBSCRIPT, pyatspi.ROLE_SUPERSCRIPT]
3923
3924    def isSwitch(self, obj):
3925        if not (obj and self.inDocumentContent(obj)):
3926            return super().isSwitch(obj)
3927
3928        return 'switch' in self._getXMLRoles(obj)
3929
3930    def isNonNavigableEmbeddedDocument(self, obj):
3931        rv = self._isNonNavigableEmbeddedDocument.get(hash(obj))
3932        if rv is not None:
3933            return rv
3934
3935        rv = False
3936        if self.isDocument(obj) and self.getDocumentForObject(obj):
3937            try:
3938                name = obj.name
3939            except:
3940                rv = True
3941            else:
3942                rv = "doubleclick" in name
3943
3944        self._isNonNavigableEmbeddedDocument[hash(obj)] = rv
3945        return rv
3946
3947    def isRedundantSVG(self, obj):
3948        if self._getTag(obj) != 'svg' or obj.parent.childCount == 1:
3949            return False
3950
3951        rv = self._isRedundantSVG.get(hash(obj))
3952        if rv is not None:
3953            return rv
3954
3955        rv = False
3956        children = [x for x in obj.parent if self._getTag(x) == 'svg']
3957        if len(children) == obj.parent.childCount:
3958            sortedChildren = sorted(children, key=functools.cmp_to_key(self.sizeComparison))
3959            if obj != sortedChildren[-1]:
3960                objExtents = self.getExtents(obj, 0, -1)
3961                largestExtents = self.getExtents(sortedChildren[-1], 0, -1)
3962                rv = self.intersection(objExtents, largestExtents) == tuple(objExtents)
3963
3964        self._isRedundantSVG[hash(obj)] = rv
3965        return rv
3966
3967    def isCustomImage(self, obj):
3968        if not (obj and self.inDocumentContent(obj)):
3969            return False
3970
3971        rv = self._isCustomImage.get(hash(obj))
3972        if rv is not None:
3973            return rv
3974
3975        rv = False
3976        if self.isCustomElement(obj) and self.hasExplicitName(obj) \
3977           and 'Text' in pyatspi.listInterfaces(obj) \
3978           and not re.search(r'[^\s\ufffc]', obj.queryText().getText(0, -1)):
3979            for child in obj:
3980                if child.getRole() not in [pyatspi.ROLE_IMAGE, pyatspi.ROLE_CANVAS] \
3981                   and self._getTag(child) != 'svg':
3982                    break
3983            else:
3984                rv = True
3985
3986        self._isCustomImage[hash(obj)] = rv
3987        return rv
3988
3989    def isUselessImage(self, obj):
3990        if not (obj and self.inDocumentContent(obj)):
3991            return False
3992
3993        rv = self._isUselessImage.get(hash(obj))
3994        if rv is not None:
3995            return rv
3996
3997        rv = True
3998        if obj.getRole() not in [pyatspi.ROLE_IMAGE, pyatspi.ROLE_CANVAS] \
3999           and self._getTag(obj) != 'svg':
4000            rv = False
4001        if rv and (obj.name or obj.description or self.hasLongDesc(obj)):
4002            rv = False
4003        if rv and (self.isClickableElement(obj) and not self.hasExplicitName(obj)):
4004            rv = False
4005        if rv and obj.getState().contains(pyatspi.STATE_FOCUSABLE):
4006            rv = False
4007        if rv and obj.parent.getRole() == pyatspi.ROLE_LINK and not self.hasExplicitName(obj):
4008            uri = self.uri(obj.parent)
4009            if uri and not uri.startswith('javascript'):
4010                rv = False
4011        if rv and 'Image' in pyatspi.listInterfaces(obj):
4012            image = obj.queryImage()
4013            if image.imageDescription:
4014                rv = False
4015            elif not self.hasExplicitName(obj) and not self.isRedundantSVG(obj):
4016                width, height = image.getImageSize()
4017                if width > 25 and height > 25:
4018                    rv = False
4019        if rv and 'Text' in pyatspi.listInterfaces(obj):
4020            rv = self.queryNonEmptyText(obj) is None
4021        if rv and obj.childCount:
4022            for i in range(min(obj.childCount, 50)):
4023                if not self.isUselessImage(obj[i]):
4024                    rv = False
4025                    break
4026
4027        self._isUselessImage[hash(obj)] = rv
4028        return rv
4029
4030    def hasValidName(self, obj):
4031        if not (obj and obj.name):
4032            return False
4033
4034        if len(obj.name.split()) > 1:
4035            return True
4036
4037        parsed = urllib.parse.parse_qs(obj.name)
4038        if len(parsed) > 2:
4039            msg = "WEB: name of %s is suspected query string" % obj
4040            debug.println(debug.LEVEL_INFO, msg, True)
4041            return False
4042
4043        if len(obj.name) == 1 and ord(obj.name) in range(0xe000, 0xf8ff):
4044            msg = "WEB: name of %s is in unicode private use area" % obj
4045            debug.println(debug.LEVEL_INFO, msg, True)
4046            return False
4047
4048        return True
4049
4050    def isUselessEmptyElement(self, obj):
4051        if not (obj and self.inDocumentContent(obj)):
4052            return False
4053
4054        rv = self._isUselessEmptyElement.get(hash(obj))
4055        if rv is not None:
4056            return rv
4057
4058        try:
4059            role = obj.getRole()
4060            state = obj.getState()
4061            interfaces = pyatspi.listInterfaces(obj)
4062        except:
4063            msg = "WEB: Exception getting role, state, and interfaces for %s" % obj
4064            debug.println(debug.LEVEL_INFO, msg, True)
4065            return False
4066
4067        roles = [pyatspi.ROLE_PARAGRAPH,
4068                 pyatspi.ROLE_SECTION,
4069                 pyatspi.ROLE_STATIC,
4070                 pyatspi.ROLE_TABLE_ROW]
4071
4072        if role not in roles and not self.isAriaAlert(obj):
4073            rv = False
4074        elif state.contains(pyatspi.STATE_FOCUSABLE) or state.contains(pyatspi.STATE_FOCUSED):
4075            rv = False
4076        elif state.contains(pyatspi.STATE_EDITABLE):
4077            rv = False
4078        elif self.hasValidName(obj) or obj.description or obj.childCount:
4079            rv = False
4080        elif "Text" in interfaces and obj.queryText().characterCount \
4081             and obj.queryText().getText(0, -1) != obj.name:
4082            rv = False
4083        elif "Action" in interfaces and self._getActionNames(obj):
4084            rv = False
4085        else:
4086            rv = True
4087
4088        self._isUselessEmptyElement[hash(obj)] = rv
4089        return rv
4090
4091    def isParentOfNullChild(self, obj):
4092        if not (obj and self.inDocumentContent(obj)):
4093            return False
4094
4095        rv = self._isParentOfNullChild.get(hash(obj))
4096        if rv is not None:
4097            return rv
4098
4099        rv = False
4100        try:
4101            childCount = obj.childCount
4102        except:
4103            msg = "WEB: Exception getting childCount for %s" % obj
4104            debug.println(debug.LEVEL_INFO, msg, True)
4105            childCount = 0
4106        if childCount and obj[0] is None:
4107            msg = "ERROR: %s reports %i children, but obj[0] is None" % (obj, childCount)
4108            debug.println(debug.LEVEL_INFO, msg, True)
4109            rv = True
4110
4111        self._isParentOfNullChild[hash(obj)] = rv
4112        return rv
4113
4114    def hasExplicitName(self, obj):
4115        if not (obj and self.inDocumentContent(obj)):
4116            return False
4117
4118        attrs = self.objectAttributes(obj)
4119        return attrs.get('explicit-name') == 'true'
4120
4121    def hasLongDesc(self, obj):
4122        if not (obj and self.inDocumentContent(obj)):
4123            return False
4124
4125        rv = self._hasLongDesc.get(hash(obj))
4126        if rv is not None:
4127            return rv
4128
4129        names = self._getActionNames(obj)
4130        rv = "showlongdesc" in names
4131
4132        self._hasLongDesc[hash(obj)] = rv
4133        return rv
4134
4135    def hasVisibleCaption(self, obj):
4136        if not (obj and self.inDocumentContent(obj)):
4137            return super().hasVisibleCaption(obj)
4138
4139        if not (self.isFigure(obj) or "Table" in pyatspi.listInterfaces(obj)):
4140            return False
4141
4142        rv = self._hasVisibleCaption.get(hash(obj))
4143        if rv is not None:
4144            return rv
4145
4146        labels = self.labelsForObject(obj)
4147        pred = lambda x: x and x.getRole() == pyatspi.ROLE_CAPTION and self.isShowingAndVisible(x)
4148        rv = bool(list(filter(pred, labels)))
4149        self._hasVisibleCaption[hash(obj)] = rv
4150        return rv
4151
4152    def hasDetails(self, obj):
4153        if not (obj and self.inDocumentContent(obj)):
4154            return super().hasDetails(obj)
4155
4156        rv = self._hasDetails.get(hash(obj))
4157        if rv is not None:
4158            return rv
4159
4160        try:
4161            relations = obj.getRelationSet()
4162        except:
4163            msg = 'ERROR: Exception getting relationset for %s' % obj
4164            debug.println(debug.LEVEL_INFO, msg, True)
4165            return False
4166
4167        rv = False
4168        relation = filter(lambda x: x.getRelationType() == pyatspi.RELATION_DETAILS, relations)
4169        for r in relation:
4170            if r.getNTargets() > 0:
4171                rv = True
4172                break
4173
4174        self._hasDetails[hash(obj)] = rv
4175        return rv
4176
4177    def detailsIn(self, obj):
4178        if not self.hasDetails(obj):
4179            return []
4180
4181        try:
4182            relations = obj.getRelationSet()
4183        except:
4184            msg = 'ERROR: Exception getting relationset for %s' % obj
4185            debug.println(debug.LEVEL_INFO, msg, True)
4186            return []
4187
4188        rv = []
4189        relation = filter(lambda x: x.getRelationType() == pyatspi.RELATION_DETAILS, relations)
4190        for r in relation:
4191            for i in range(r.getNTargets()):
4192                rv.append(r.getTarget(i))
4193
4194        return rv
4195
4196    def isDetails(self, obj):
4197        if not (obj and self.inDocumentContent(obj)):
4198            return super().isDetails(obj)
4199
4200        rv = self._isDetails.get(hash(obj))
4201        if rv is not None:
4202            return rv
4203
4204        try:
4205            relations = obj.getRelationSet()
4206        except:
4207            msg = 'ERROR: Exception getting relationset for %s' % obj
4208            debug.println(debug.LEVEL_INFO, msg, True)
4209            return False
4210
4211        rv = False
4212        relation = filter(lambda x: x.getRelationType() == pyatspi.RELATION_DETAILS_FOR, relations)
4213        for r in relation:
4214            if r.getNTargets() > 0:
4215                rv = True
4216                break
4217
4218        self._isDetails[hash(obj)] = rv
4219        return rv
4220
4221    def detailsFor(self, obj):
4222        if not self.isDetails(obj):
4223            return []
4224
4225        try:
4226            relations = obj.getRelationSet()
4227        except:
4228            msg = 'ERROR: Exception getting relationset for %s' % obj
4229            debug.println(debug.LEVEL_INFO, msg, True)
4230            return []
4231
4232        rv = []
4233        relation = filter(lambda x: x.getRelationType() == pyatspi.RELATION_DETAILS_FOR, relations)
4234        for r in relation:
4235            for i in range(r.getNTargets()):
4236                rv.append(r.getTarget(i))
4237
4238        return rv
4239
4240    def popupType(self, obj):
4241        if not (obj and self.inDocumentContent(obj)):
4242            return 'false'
4243
4244        attrs = self.objectAttributes(obj)
4245        return attrs.get('haspopup', 'false').lower()
4246
4247    def inferLabelFor(self, obj):
4248        if not self.shouldInferLabelFor(obj):
4249            return None, []
4250
4251        rv = self._inferredLabels.get(hash(obj))
4252        if rv is not None:
4253            return rv
4254
4255        rv = self._script.labelInference.infer(obj, False)
4256        self._inferredLabels[hash(obj)] = rv
4257        return rv
4258
4259    def shouldInferLabelFor(self, obj):
4260        if not self.inDocumentContent() or self.isWebAppDescendant(obj):
4261            return False
4262
4263        rv = self._shouldInferLabelFor.get(hash(obj))
4264        if rv and not self._script._lastCommandWasCaretNav:
4265            return not self._script.inSayAll()
4266        if rv == False:
4267            return rv
4268
4269        try:
4270            role = obj.getRole()
4271            name = obj.name
4272        except:
4273            msg = "WEB: Exception getting role and name for %s" % obj
4274            debug.println(debug.LEVEL_INFO, msg, True)
4275            rv = False
4276        else:
4277            if name:
4278                rv = False
4279            elif not rv:
4280                roles =  [pyatspi.ROLE_CHECK_BOX,
4281                          pyatspi.ROLE_COMBO_BOX,
4282                          pyatspi.ROLE_ENTRY,
4283                          pyatspi.ROLE_LIST_BOX,
4284                          pyatspi.ROLE_PASSWORD_TEXT,
4285                          pyatspi.ROLE_RADIO_BUTTON]
4286                rv = role in roles and not self.displayedLabel(obj)
4287
4288        self._shouldInferLabelFor[hash(obj)] = rv
4289
4290        # TODO - JD: This is private.
4291        if self._script._lastCommandWasCaretNav \
4292           and role not in [pyatspi.ROLE_RADIO_BUTTON, pyatspi.ROLE_CHECK_BOX]:
4293            return False
4294
4295        return rv
4296
4297    def displayedLabel(self, obj):
4298        if not (obj and self.inDocumentContent(obj)):
4299            return super().displayedLabel(obj)
4300
4301        rv = self._displayedLabelText.get(hash(obj))
4302        if rv is not None:
4303            return rv
4304
4305        labels = self.labelsForObject(obj)
4306        strings = [l.name or self.displayedText(l) for l in labels if l is not None]
4307        rv = " ".join(strings)
4308
4309        self._displayedLabelText[hash(obj)] = rv
4310        return rv
4311
4312    def labelsForObject(self, obj):
4313        if not obj:
4314            return []
4315
4316        rv = self._labelsForObject.get(hash(obj))
4317        if rv is not None:
4318            return rv
4319
4320        rv = super().labelsForObject(obj)
4321        if not self.inDocumentContent(obj):
4322            return rv
4323
4324        self._labelsForObject[hash(obj)] = rv
4325        return rv
4326
4327    def isSpinnerEntry(self, obj):
4328        if not self.inDocumentContent(obj):
4329            return False
4330
4331        if not obj.getState().contains(pyatspi.STATE_EDITABLE):
4332            return False
4333
4334        if pyatspi.ROLE_SPIN_BUTTON in [obj.getRole(), obj.parent.getRole()]:
4335            return True
4336
4337        return False
4338
4339    def eventIsSpinnerNoise(self, event):
4340        if not self.isSpinnerEntry(event.source):
4341            return False
4342
4343        if event.type.startswith("object:text-changed") \
4344           or event.type.startswith("object:text-selection-changed"):
4345            lastKey, mods = self.lastKeyAndModifiers()
4346            if lastKey in ["Down", "Up"]:
4347                return True
4348
4349        return False
4350
4351    def treatEventAsSpinnerValueChange(self, event):
4352        if event.type.startswith("object:text-caret-moved") and self.isSpinnerEntry(event.source):
4353            lastKey, mods = self.lastKeyAndModifiers()
4354            if lastKey in ["Down", "Up"]:
4355                obj, offset = self.getCaretContext()
4356                return event.source == obj
4357
4358        return False
4359
4360    def eventIsBrowserUINoise(self, event):
4361        if self.inDocumentContent(event.source):
4362            return False
4363
4364        try:
4365            role = event.source.getRole()
4366        except:
4367            msg = "WEB: Exception getting role for %s" % event.source
4368            debug.println(debug.LEVEL_INFO, msg, True)
4369            return False
4370
4371        eType = event.type
4372        if eType.startswith("object:text-") and self.isSingleLineAutocompleteEntry(event.source):
4373            lastKey, mods = self.lastKeyAndModifiers()
4374            return lastKey == "Return"
4375        if eType.startswith("object:text-") or eType.endswith("accessible-name"):
4376            return role in [pyatspi.ROLE_STATUS_BAR, pyatspi.ROLE_LABEL]
4377        if eType.startswith("object:children-changed"):
4378            return True
4379
4380        return False
4381
4382    def eventIsAutocompleteNoise(self, event, documentFrame=None):
4383        inContent = documentFrame or self.inDocumentContent(event.source)
4384        if not inContent:
4385            return False
4386
4387        isListBoxItem = lambda x: x and x.parent and x.parent.getRole() == pyatspi.ROLE_LIST_BOX
4388        isMenuItem = lambda x: x and x.parent and x.parent.getRole() == pyatspi.ROLE_MENU
4389        isComboBoxItem = lambda x: x and x.parent and x.parent.getRole() == pyatspi.ROLE_COMBO_BOX
4390
4391        if event.source.getState().contains(pyatspi.STATE_EDITABLE) \
4392           and event.type.startswith("object:text-"):
4393            obj, offset = self.getCaretContext(documentFrame)
4394            if isListBoxItem(obj) or isMenuItem(obj):
4395                return True
4396
4397            if obj == event.source and isComboBoxItem(obj):
4398                lastKey, mods = self.lastKeyAndModifiers()
4399                if lastKey in ["Down", "Up"]:
4400                    return True
4401
4402        return False
4403
4404    def eventIsBrowserUIAutocompleteNoise(self, event):
4405        if self.inDocumentContent(event.source):
4406            return False
4407
4408        if self._eventIsBrowserUIAutocompleteTextNoise(event):
4409            return True
4410
4411        return self._eventIsBrowserUIAutocompleteSelectionNoise(event)
4412
4413    def _eventIsBrowserUIAutocompleteSelectionNoise(self, event):
4414        selection = ["object:selection-changed", "object:state-changed:selected"]
4415        if not event.type in selection:
4416            return False
4417
4418        try:
4419            focusRole = orca_state.locusOfFocus.getRole()
4420            focusState = orca_state.locusOfFocus.getState()
4421        except:
4422            msg = "WEB: Exception getting role and state for %s" % orca_state.locusOfFocus
4423            debug.println(debug.LEVEL_INFO, msg, True)
4424            return False
4425
4426        try:
4427            role = event.source.getRole()
4428        except:
4429            msg = "WEB: Exception getting role for %s" % event.source
4430            debug.println(debug.LEVEL_INFO, msg, True)
4431            return False
4432
4433        if role in [pyatspi.ROLE_MENU, pyatspi.ROLE_MENU_ITEM] \
4434           and focusRole == pyatspi.ROLE_ENTRY \
4435           and focusState.contains(pyatspi.STATE_FOCUSED):
4436            lastKey, mods = self.lastKeyAndModifiers()
4437            if lastKey not in ["Down", "Up"]:
4438                return True
4439
4440        return False
4441
4442    def _eventIsBrowserUIAutocompleteTextNoise(self, event):
4443        if not event.type.startswith("object:text-") \
4444           or not orca_state.locusOfFocus \
4445           or not self.isSingleLineAutocompleteEntry(event.source):
4446            return False
4447
4448        roles = [pyatspi.ROLE_MENU_ITEM,
4449                 pyatspi.ROLE_CHECK_MENU_ITEM,
4450                 pyatspi.ROLE_RADIO_MENU_ITEM,
4451                 pyatspi.ROLE_LIST_ITEM]
4452
4453        if orca_state.locusOfFocus.getRole() in roles \
4454           and orca_state.locusOfFocus.getState().contains(pyatspi.STATE_SELECTABLE):
4455            lastKey, mods = self.lastKeyAndModifiers()
4456            return lastKey in ["Down", "Up"]
4457
4458        return False
4459
4460    def eventIsBrowserUIPageSwitch(self, event):
4461        selection = ["object:selection-changed", "object:state-changed:selected"]
4462        if not event.type in selection:
4463            return False
4464
4465        roles = [pyatspi.ROLE_PAGE_TAB, pyatspi.ROLE_PAGE_TAB_LIST]
4466        if not event.source.getRole() in roles:
4467            return False
4468
4469        if self.inDocumentContent(event.source):
4470            return False
4471
4472        if not self.inDocumentContent(orca_state.locusOfFocus):
4473            return False
4474
4475        return True
4476
4477    def eventIsFromLocusOfFocusDocument(self, event):
4478        if orca_state.locusOfFocus == orca_state.activeWindow:
4479            focus = self.activeDocument()
4480            source = self.getTopLevelDocumentForObject(event.source)
4481        else:
4482            focus = self.getDocumentForObject(orca_state.locusOfFocus)
4483            source = self.getDocumentForObject(event.source)
4484
4485        msg = "WEB: Event doc: %s. Focus doc: %s." % (source, focus)
4486        debug.println(debug.LEVEL_INFO, msg, True)
4487
4488        if not (source and focus):
4489            return False
4490
4491        if source == focus:
4492            return True
4493
4494        if self.isZombie(focus) and not self.isZombie(source):
4495            if self.activeDocument() == source:
4496                msg = "WEB: Treating active doc as locusOfFocus doc"
4497                debug.println(debug.LEVEL_INFO, msg, True)
4498                return True
4499
4500        return False
4501
4502    def textEventIsDueToDeletion(self, event):
4503        if not self.inDocumentContent(event.source) \
4504           or not event.source.getState().contains(pyatspi.STATE_EDITABLE):
4505            return False
4506
4507        if self.isDeleteCommandTextDeletionEvent(event) \
4508           or self.isBackSpaceCommandTextDeletionEvent(event):
4509            return True
4510
4511        return False
4512
4513    def textEventIsDueToInsertion(self, event):
4514        if not event.type.startswith("object:text-"):
4515            return False
4516
4517        if not self.inDocumentContent(event.source) \
4518           or not event.source.getState().contains(pyatspi.STATE_EDITABLE) \
4519           or not event.source == orca_state.locusOfFocus:
4520            return False
4521
4522        if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
4523            inputEvent = orca_state.lastNonModifierKeyEvent
4524            return inputEvent and inputEvent.isPrintableKey() and not inputEvent.modifiers
4525
4526        return False
4527
4528    def textEventIsForNonNavigableTextObject(self, event):
4529        if not event.type.startswith("object:text-"):
4530            return False
4531
4532        return self._treatObjectAsWhole(event.source)
4533
4534    def eventIsEOCAdded(self, event):
4535        if not self.inDocumentContent(event.source):
4536            return False
4537
4538        if event.type.startswith("object:text-changed:insert") \
4539           and self.EMBEDDED_OBJECT_CHARACTER in event.any_data:
4540            return not re.match("[^\s\ufffc]", event.any_data)
4541
4542        return False
4543
4544    def caretMovedOutsideActiveGrid(self, event, oldFocus=None):
4545        if not (event and event.type.startswith("object:text-caret-moved")):
4546            return False
4547
4548        oldFocus = oldFocus or orca_state.locusOfFocus
4549        if not self.isGridDescendant(oldFocus):
4550            return False
4551
4552        return not self.isGridDescendant(event.source)
4553
4554    def caretMovedToSamePageFragment(self, event, oldFocus=None):
4555        if not (event and event.type.startswith("object:text-caret-moved")):
4556            return False
4557
4558        if event.source.getState().contains(pyatspi.STATE_EDITABLE):
4559            return False
4560
4561        docURI = self.documentFrameURI()
4562        fragment = urllib.parse.urlparse(docURI).fragment
4563        if not fragment:
4564            return False
4565
4566        sourceID = self._getID(event.source)
4567        if sourceID and fragment == sourceID:
4568            return True
4569
4570        oldFocus = oldFocus or orca_state.locusOfFocus
4571        if self.isLink(oldFocus):
4572            link = oldFocus
4573        else:
4574            link = pyatspi.findAncestor(oldFocus, self.isLink)
4575
4576        return link and self.uri(link) == docURI
4577
4578    def isChildOfCurrentFragment(self, obj):
4579        parseResult = urllib.parse.urlparse(self.documentFrameURI())
4580        if not parseResult.fragment:
4581            return False
4582
4583        isSameFragment = lambda x: self._getID(x) == parseResult.fragment
4584        return pyatspi.findAncestor(obj, isSameFragment) is not None
4585
4586    def documentFragment(self, documentFrame):
4587        parseResult = urllib.parse.urlparse(self.documentFrameURI(documentFrame))
4588        return parseResult.fragment
4589
4590    def isContentEditableWithEmbeddedObjects(self, obj):
4591        if not (obj and self.inDocumentContent(obj)):
4592            return False
4593
4594        rv = self._isContentEditableWithEmbeddedObjects.get(hash(obj))
4595        if rv is not None:
4596            return rv
4597
4598        rv = False
4599        try:
4600            state = obj.getState()
4601            role = obj.getRole()
4602        except:
4603            msg = "WEB: Exception getting state and role for %s" % obj
4604            debug.println(debug.LEVEL_INFO, msg, True)
4605            return rv
4606
4607        hasTextBlockRole = lambda x: x and x.getRole() in self._textBlockElementRoles() \
4608            and not self.isFakePlaceholderForEntry(x) and not self.isStaticTextLeaf(x)
4609
4610        if self._getTag(obj) in ["input", "textarea"]:
4611            rv = False
4612        elif role == pyatspi.ROLE_ENTRY and state.contains(pyatspi.STATE_MULTI_LINE):
4613            rv = pyatspi.findDescendant(obj, hasTextBlockRole)
4614        elif state.contains(pyatspi.STATE_EDITABLE):
4615            rv = hasTextBlockRole(obj) or self.isLink(obj)
4616        elif not self.isDocument(obj):
4617            document = self.getDocumentForObject(obj)
4618            rv = self.isContentEditableWithEmbeddedObjects(document)
4619
4620        self._isContentEditableWithEmbeddedObjects[hash(obj)] = rv
4621        return rv
4622
4623    def characterOffsetInParent(self, obj):
4624        start, end, length = self._rangeInParentWithLength(obj)
4625        return start
4626
4627    def _rangeInParentWithLength(self, obj):
4628        if not obj:
4629            return -1, -1, 0
4630
4631        text = self.queryNonEmptyText(obj.parent)
4632        if not text:
4633            return -1, -1, 0
4634
4635        start, end = self.getHyperlinkRange(obj)
4636        return start, end, text.characterCount
4637
4638    def getError(self, obj):
4639        if not (obj and self.inDocumentContent(obj)):
4640            return super().getError(obj)
4641
4642        try:
4643            state = obj.getState()
4644        except:
4645            msg = "ERROR: Exception getting state for %s" % obj
4646            debug.println(debug.LEVEL_INFO, msg, True)
4647            return False
4648
4649        if not state.contains(pyatspi.STATE_INVALID_ENTRY):
4650            return False
4651
4652        try:
4653            self._currentTextAttrs.pop(hash(obj))
4654        except:
4655            pass
4656
4657        attrs, start, end = self.textAttributes(obj, 0, True)
4658        error = attrs.get("invalid")
4659        if error == "false":
4660            return False
4661        if error not in ["spelling", "grammar"]:
4662            return True
4663
4664        return error
4665
4666    def _getErrorMessageContainer(self, obj):
4667        if not (obj and self.inDocumentContent(obj)):
4668            return None
4669
4670        if not self.getError(obj):
4671            return None
4672
4673        # Remove this when we bump dependencies to 2.26
4674        try:
4675            relationType = pyatspi.RELATION_ERROR_MESSAGE
4676        except:
4677            return None
4678
4679        isMessage = lambda r: r.getRelationType() == relationType
4680        relations = list(filter(isMessage, obj.getRelationSet()))
4681        if not relations:
4682            return None
4683
4684        return relations[0].getTarget(0)
4685
4686    def getErrorMessage(self, obj):
4687        return self.expandEOCs(self._getErrorMessageContainer(obj))
4688
4689    def isErrorForContents(self, obj, contents=[]):
4690        if not self.isErrorMessage(obj):
4691            return False
4692
4693        for acc, start, end, string in contents:
4694            if self._getErrorMessageContainer(acc) == obj:
4695                return True
4696
4697        return False
4698
4699    def hasNoSize(self, obj):
4700        if not (obj and self.inDocumentContent(obj)):
4701            return super().hasNoSize(obj)
4702
4703        rv = self._hasNoSize.get(hash(obj))
4704        if rv is not None:
4705            return rv
4706
4707        rv = super().hasNoSize(obj)
4708        self._hasNoSize[hash(obj)] = rv
4709        return rv
4710
4711    def _canHaveCaretContext(self, obj):
4712        if not obj:
4713            return False
4714        if self.isDead(obj):
4715            msg = "WEB: Dead object cannot have caret context %s" % obj
4716            debug.println(debug.LEVEL_INFO, msg, True)
4717            return False
4718        if self.isZombie(obj):
4719            msg = "WEB: Zombie object cannot have caret context %s" % obj
4720            debug.println(debug.LEVEL_INFO, msg, True)
4721            return False
4722        if self.isHidden(obj):
4723            msg = "WEB: Hidden object cannot have caret context %s" % obj
4724            debug.println(debug.LEVEL_INFO, msg, True)
4725            return False
4726        if self.isOffScreenLabel(obj):
4727            msg = "WEB: Off-screen label cannot have caret context %s" % obj
4728            debug.println(debug.LEVEL_INFO, msg, True)
4729            return False
4730        if self.isNonNavigablePopup(obj):
4731            msg = "WEB: Non-navigable popup cannot have caret context %s" % obj
4732            debug.println(debug.LEVEL_INFO, msg, True)
4733            return False
4734        if self.isUselessImage(obj):
4735            msg = "WEB: Useless image cannot have caret context %s" % obj
4736            debug.println(debug.LEVEL_INFO, msg, True)
4737            return False
4738        if self.isUselessEmptyElement(obj):
4739            msg = "WEB: Useless empty element cannot have caret context %s" % obj
4740            debug.println(debug.LEVEL_INFO, msg, True)
4741            return False
4742        if self.isEmptyAnchor(obj):
4743            msg = "WEB: Empty anchor cannot have caret context %s" % obj
4744            debug.println(debug.LEVEL_INFO, msg, True)
4745            return False
4746        if self.isEmptyToolTip(obj):
4747            msg = "WEB: Empty tool tip cannot have caret context %s" % obj
4748            debug.println(debug.LEVEL_INFO, msg, True)
4749            return False
4750        if self.hasNoSize(obj):
4751            msg = "WEB: Allowing sizeless object to have caret context %s" % obj
4752            debug.println(debug.LEVEL_INFO, msg, True)
4753            return True
4754        if self.isParentOfNullChild(obj):
4755            msg = "WEB: Parent of null child cannot have caret context %s" % obj
4756            debug.println(debug.LEVEL_INFO, msg, True)
4757            return False
4758        if self.isPseudoElement(obj):
4759            msg = "WEB: Pseudo element cannot have caret context %s" % obj
4760            debug.println(debug.LEVEL_INFO, msg, True)
4761            return False
4762        if self.isStaticTextLeaf(obj):
4763            msg = "WEB: Static text leaf cannot have caret context %s" % obj
4764            debug.println(debug.LEVEL_INFO, msg, True)
4765            return False
4766        if self.isFakePlaceholderForEntry(obj):
4767            msg = "WEB: Fake placeholder for entry cannot have caret context %s" % obj
4768            debug.println(debug.LEVEL_INFO, msg, True)
4769            return False
4770        if self.isNonInteractiveDescendantOfControl(obj):
4771            msg = "WEB: Non interactive descendant of control cannot have caret context %s" % obj
4772            debug.println(debug.LEVEL_INFO, msg, True)
4773            return False
4774
4775        return True
4776
4777    def isPseudoElement(self, obj):
4778        return False
4779
4780    def searchForCaretContext(self, obj):
4781        msg = "WEB: Searching for caret context in %s" % obj
4782        debug.println(debug.LEVEL_INFO, msg, True)
4783
4784        container = obj
4785        contextObj, contextOffset = None, -1
4786        while obj:
4787            try:
4788                offset = obj.queryText().caretOffset
4789            except:
4790                msg = "WEB: Exception getting caret offset of %s" % obj
4791                debug.println(debug.LEVEL_INFO, msg, True)
4792                obj = None
4793            else:
4794                contextObj, contextOffset = obj, offset
4795                child = self.getChildAtOffset(obj, offset)
4796                if child:
4797                    obj = child
4798                else:
4799                    break
4800
4801        if contextObj and not self.isHidden(contextObj):
4802            return self.findNextCaretInOrder(contextObj, max(-1, contextOffset - 1))
4803
4804        if self.isDocument(container):
4805            return container, 0
4806
4807        return None, -1
4808
4809    def _getCaretContextViaLocusOfFocus(self):
4810        obj = orca_state.locusOfFocus
4811        if not self.inDocumentContent(obj):
4812            return None, -1
4813
4814        try:
4815            offset = obj.queryText().caretOffset
4816        except NotImplementedError:
4817            offset = 0
4818        except:
4819            offset = -1
4820
4821        return obj, offset
4822
4823    def getCaretContext(self, documentFrame=None, getZombieReplicant=False, searchIfNeeded=True):
4824        msg = "WEB: Getting caret context for %s" % documentFrame
4825        debug.println(debug.LEVEL_INFO, msg, True)
4826
4827        if not documentFrame or self.isZombie(documentFrame):
4828            documentFrame = self.documentFrame()
4829
4830        if not documentFrame:
4831            if not searchIfNeeded:
4832                msg = "WEB: Returning None, -1: No document and no search requested."
4833                debug.println(debug.LEVEL_INFO, msg, True)
4834                return None, -1
4835
4836            obj, offset = self._getCaretContextViaLocusOfFocus()
4837            msg = "WEB: Returning %s, %i (from locusOfFocus)" % (obj, offset)
4838            debug.println(debug.LEVEL_INFO, msg, True)
4839            return obj, offset
4840
4841        context = self._caretContexts.get(hash(documentFrame.parent))
4842        if not context or not self.isTopLevelDocument(documentFrame):
4843            if not searchIfNeeded:
4844                return None, -1
4845            obj, offset = self.searchForCaretContext(documentFrame)
4846        elif not getZombieReplicant:
4847            return context
4848        elif self.isZombie(context[0]):
4849            msg = "WEB: Context is Zombie. Searching for replicant."
4850            debug.println(debug.LEVEL_INFO, msg, True)
4851            obj, offset = self.findContextReplicant()
4852            if obj:
4853                caretObj, caretOffset = self.searchForCaretContext(obj.parent)
4854                if caretObj and not self.isZombie(caretObj):
4855                    obj, offset = caretObj, caretOffset
4856        else:
4857            obj, offset = context
4858
4859        self.setCaretContext(obj, offset, documentFrame)
4860
4861        return obj, offset
4862
4863    def getCaretContextPathRoleAndName(self, documentFrame=None):
4864        documentFrame = documentFrame or self.documentFrame()
4865        if not documentFrame:
4866            return [-1], None, None
4867
4868        rv = self._contextPathsRolesAndNames.get(hash(documentFrame.parent))
4869        if not rv:
4870            return [-1], None, None
4871
4872        return rv
4873
4874    def getObjectFromPath(self, path):
4875        start = self._script.app
4876        rv = None
4877        for p in path:
4878            if p == -1:
4879                continue
4880            try:
4881                start = start[p]
4882            except:
4883                break
4884        else:
4885            rv = start
4886
4887        return rv
4888
4889    def clearCaretContext(self, documentFrame=None):
4890        self.clearContentCache()
4891        documentFrame = documentFrame or self.documentFrame()
4892        if not documentFrame:
4893            return
4894
4895        parent = documentFrame.parent
4896        self._caretContexts.pop(hash(parent), None)
4897        self._priorContexts.pop(hash(parent), None)
4898
4899    def handleEventFromContextReplicant(self, event, replicant):
4900        if self.isDead(replicant):
4901            msg = "WEB: Context replicant is dead."
4902            debug.println(debug.LEVEL_INFO, msg, True)
4903            return False
4904
4905        if not self.isDead(orca_state.locusOfFocus):
4906            msg = "WEB: Not event from context replicant. locusOfFocus %s is not dead." \
4907                % orca_state.locusOfFocus
4908            debug.println(debug.LEVEL_INFO, msg, True)
4909            return False
4910
4911        path, role, name = self.getCaretContextPathRoleAndName()
4912        replicantPath = pyatspi.getPath(replicant)
4913        if path != replicantPath:
4914            msg = "WEB: Not event from context replicant. Path %s != replicant path %s." \
4915                % (path, replicantPath)
4916            return False
4917
4918        replicantRole = replicant.getRole()
4919        if role != replicantRole:
4920            msg = "WEB: Not event from context replicant. Role %s != replicant role %s." \
4921                % (role, replicantRole)
4922            return False
4923
4924        notify = replicant.name != name
4925        documentFrame = self.documentFrame()
4926        obj, offset = self._caretContexts.get(hash(documentFrame.parent))
4927
4928        msg = "WEB: Is event from context replicant. Notify: %s" % notify
4929        debug.println(debug.LEVEL_INFO, msg, True)
4930
4931        orca.setLocusOfFocus(event, replicant, notify)
4932        self.setCaretContext(replicant, offset, documentFrame)
4933        return True
4934
4935    def handleEventForRemovedChild(self, event):
4936        if event.any_data == orca_state.locusOfFocus:
4937            msg = "WEB: Removed child is locusOfFocus."
4938            debug.println(debug.LEVEL_INFO, msg, True)
4939        elif pyatspi.findAncestor(orca_state.locusOfFocus, lambda x: x == event.any_data):
4940            msg = "WEB: Removed child is ancestor of locusOfFocus."
4941            debug.println(debug.LEVEL_INFO, msg, True)
4942        else:
4943            return False
4944
4945        if event.detail1 == -1:
4946            msg = "WEB: Event detail1 is useless."
4947            debug.println(debug.LEVEL_INFO, msg, True)
4948            return False
4949
4950        obj, offset = None, -1
4951        notify = True
4952        keyString, mods = self.lastKeyAndModifiers()
4953        if keyString == "Up":
4954            if event.detail1 >= event.source.childCount:
4955                msg = "WEB: Last child removed. Getting new location from end of parent."
4956                debug.println(debug.LEVEL_INFO, msg, True)
4957                obj, offset = self.previousContext(event.source, -1)
4958            elif 0 <= event.detail1 - 1 < event.source.childCount:
4959                child = event.source[event.detail1 - 1]
4960                msg = "WEB: Getting new location from end of previous child %s." % child
4961                debug.println(debug.LEVEL_INFO, msg, True)
4962                obj, offset = self.previousContext(child, -1)
4963            else:
4964                prevObj = self.findPreviousObject(event.source)
4965                msg = "WEB: Getting new location from end of source's previous object %s." % prevObj
4966                debug.println(debug.LEVEL_INFO, msg, True)
4967                obj, offset = self.previousContext(prevObj, -1)
4968
4969        elif keyString == "Down":
4970            if event.detail1 == 0:
4971                msg = "WEB: First child removed. Getting new location from start of parent."
4972                debug.println(debug.LEVEL_INFO, msg, True)
4973                obj, offset = self.nextContext(event.source, -1)
4974            elif 0 < event.detail1 < event.source.childCount:
4975                child = event.source[event.detail1]
4976                msg = "WEB: Getting new location from start of child %i %s." % (event.detail1, child)
4977                debug.println(debug.LEVEL_INFO, msg, True)
4978                obj, offset = self.nextContext(child, -1)
4979            else:
4980                nextObj = self.findNextObject(event.source)
4981                msg = "WEB: Getting new location from start of source's next object %s." % nextObj
4982                debug.println(debug.LEVEL_INFO, msg, True)
4983                obj, offset = self.nextContext(nextObj, -1)
4984
4985        else:
4986            notify = False
4987            obj, offset = self.searchForCaretContext(event.source)
4988
4989        if obj:
4990            msg = "WEB: Setting locusOfFocus and context to: %s, %i" % (obj, offset)
4991            orca.setLocusOfFocus(event, obj, notify)
4992            self.setCaretContext(obj, offset)
4993            return True
4994
4995        msg = "WEB: Unable to find context for child removed from %s" % event.source
4996        debug.println(debug.LEVEL_INFO, msg, True)
4997        return False
4998
4999    def findContextReplicant(self, documentFrame=None, matchRole=True, matchName=True):
5000        path, oldRole, oldName = self.getCaretContextPathRoleAndName(documentFrame)
5001        obj = self.getObjectFromPath(path)
5002        if obj and matchRole:
5003            if obj.getRole() != oldRole:
5004                obj = None
5005        if obj and matchName:
5006            if obj.name != oldName:
5007                obj = None
5008        if not obj:
5009            return None, -1
5010
5011        obj, offset = self.findFirstCaretContext(obj, 0)
5012        msg = "WEB: Context replicant is %s, %i" % (obj, offset)
5013        debug.println(debug.LEVEL_INFO, msg, True)
5014        return obj, offset
5015
5016    def getPriorContext(self, documentFrame=None):
5017        if not documentFrame or self.isZombie(documentFrame):
5018            documentFrame = self.documentFrame()
5019
5020        if documentFrame:
5021            context = self._priorContexts.get(hash(documentFrame.parent))
5022            if context:
5023                return context
5024
5025        return None, -1
5026
5027    def _getPath(self, obj):
5028        rv = self._paths.get(hash(obj))
5029        if rv is not None:
5030            return rv
5031
5032        try:
5033            rv = pyatspi.getPath(obj)
5034        except:
5035            msg = "WEB: Exception getting path for %s" % obj
5036            debug.println(debug.LEVEL_INFO, msg, True)
5037            rv = [-1]
5038
5039        self._paths[hash(obj)] = rv
5040        return rv
5041
5042    def setCaretContext(self, obj=None, offset=-1, documentFrame=None):
5043        documentFrame = documentFrame or self.documentFrame()
5044        if not documentFrame:
5045            return
5046
5047        parent = documentFrame.parent
5048        oldObj, oldOffset = self._caretContexts.get(hash(parent), (obj, offset))
5049        self._priorContexts[hash(parent)] = oldObj, oldOffset
5050        self._caretContexts[hash(parent)] = obj, offset
5051
5052        path = self._getPath(obj)
5053        try:
5054            role = obj.getRole()
5055            name = obj.name
5056        except:
5057            msg = "WEB: Exception getting role and name for %s" % obj
5058            debug.println(debug.LEVEL_INFO, msg, True)
5059            role = None
5060            name = None
5061
5062        self._contextPathsRolesAndNames[hash(parent)] = path, role, name
5063
5064    def findFirstCaretContext(self, obj, offset):
5065        msg = "WEB: Looking for first caret context for %s, %i" % (obj, offset)
5066        debug.println(debug.LEVEL_INFO, msg, True)
5067
5068        try:
5069            role = obj.getRole()
5070        except:
5071            msg = "WEB: Exception getting first caret context for %s %i" % (obj, offset)
5072            debug.println(debug.LEVEL_INFO, msg, True)
5073            return None, -1
5074
5075        lookInChild = [pyatspi.ROLE_LIST,
5076                       pyatspi.ROLE_INTERNAL_FRAME,
5077                       pyatspi.ROLE_TABLE,
5078                       pyatspi.ROLE_TABLE_ROW]
5079        if role in lookInChild and obj.childCount and not self.treatAsDiv(obj, offset):
5080            msg = "WEB: First caret context for %s, %i will look in child %s" % (obj, offset, obj[0])
5081            debug.println(debug.LEVEL_INFO, msg, True)
5082            return self.findFirstCaretContext(obj[0], 0)
5083
5084        text = self.queryNonEmptyText(obj)
5085        if not text and self._canHaveCaretContext(obj):
5086            msg = "WEB: First caret context for non-text context %s, %i is %s, %i" % (obj, offset, obj, 0)
5087            debug.println(debug.LEVEL_INFO, msg, True)
5088            return obj, 0
5089
5090        if text and offset >= text.characterCount:
5091            if self.isContentEditableWithEmbeddedObjects(obj) and self.lastInputEventWasCharNav():
5092                nextObj, nextOffset = self.nextContext(obj, text.characterCount)
5093                if not nextObj:
5094                    msg = "WEB: No next object found at end of contenteditable %s" % obj
5095                    debug.println(debug.LEVEL_INFO, msg, True)
5096                elif not self.isContentEditableWithEmbeddedObjects(nextObj):
5097                    msg = "WEB: Next object found at end of contenteditable %s is not editable %s" % (obj, nextObj)
5098                    debug.println(debug.LEVEL_INFO, msg, True)
5099                else:
5100                    msg = "WEB: First caret context at end of contenteditable %s is next context %s, %i" % \
5101                        (obj, nextObj, nextOffset)
5102                    debug.println(debug.LEVEL_INFO, msg, True)
5103                    return nextObj, nextOffset
5104
5105            msg = "WEB: First caret context at end of %s, %i is %s, %i" % (obj, offset, obj, text.characterCount)
5106            debug.println(debug.LEVEL_INFO, msg, True)
5107            return obj, text.characterCount
5108
5109        offset = max (0, offset)
5110        if text:
5111            allText = text.getText(0, -1)
5112            if allText[offset] != self.EMBEDDED_OBJECT_CHARACTER or role == pyatspi.ROLE_ENTRY:
5113                msg = "WEB: First caret context for %s, %i is unchanged" % (obj, offset)
5114                debug.println(debug.LEVEL_INFO, msg, True)
5115                return obj, offset
5116
5117            # Descending an element that we're treating as a whole can lead to looping/getting stuck.
5118            if self.elementLinesAreSingleChars(obj):
5119                msg = "WEB: EOC in single-char-lines element. Returning %s, %i unchanged." % (obj, offset)
5120                debug.println(debug.LEVEL_INFO, msg, True)
5121                return obj, offset
5122
5123        child = self.getChildAtOffset(obj, offset)
5124        if not child:
5125            msg = "WEB: Child at offset is null. Returning %s, %i unchanged." % (obj, offset)
5126            debug.println(debug.LEVEL_INFO, msg, True)
5127            return obj, offset
5128
5129        if self.isDocument(obj):
5130            while self.isUselessEmptyElement(child):
5131                msg = "WEB: Child %s of %s at offset %i cannot be context." % (child, obj, offset)
5132                debug.println(debug.LEVEL_INFO, msg, True)
5133                offset += 1
5134                child = self.getChildAtOffset(obj, offset)
5135
5136        if self.isListItemMarker(child):
5137            msg = "WEB: First caret context for %s, %i is %s, %i (skip list item marker child)" % \
5138                (obj, offset, obj, offset + 1)
5139            debug.println(debug.LEVEL_INFO, msg, True)
5140            return obj, offset + 1
5141
5142        if self.isEmptyAnchor(child):
5143            nextObj, nextOffset = self.nextContext(obj, offset)
5144            if nextObj:
5145                msg = "WEB: First caret context at end of empty anchor %s is next context %s, %i" % \
5146                    (obj, nextObj, nextOffset)
5147                debug.println(debug.LEVEL_INFO, msg, True)
5148                return nextObj, nextOffset
5149
5150        if not self._canHaveCaretContext(child):
5151            msg = "WEB: Child cannot be context. Returning %s, %i." % (obj, offset)
5152            debug.println(debug.LEVEL_INFO, msg, True)
5153            return obj, offset
5154
5155        msg = "WEB: Looking in child %s for first caret context for %s, %i" % (child, obj, offset)
5156        debug.println(debug.LEVEL_INFO, msg, True)
5157        return self.findFirstCaretContext(child, 0)
5158
5159    def findNextCaretInOrder(self, obj=None, offset=-1):
5160        if not obj:
5161            obj, offset = self.getCaretContext()
5162
5163        if not obj or not self.inDocumentContent(obj):
5164            return None, -1
5165
5166        if self._canHaveCaretContext(obj):
5167            text = self.queryNonEmptyText(obj)
5168            if text:
5169                allText = text.getText(0, -1)
5170                for i in range(offset + 1, len(allText)):
5171                    child = self.getChildAtOffset(obj, i)
5172                    if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
5173                        msg = "ERROR: Child %s found at offset with char '%s'" % \
5174                            (child, allText[i].replace("\n", "\\n"))
5175                        debug.println(debug.LEVEL_INFO, msg, True)
5176                    if self._canHaveCaretContext(child):
5177                        if self._treatObjectAsWhole(child, -1):
5178                            return child, 0
5179                        return self.findNextCaretInOrder(child, -1)
5180                    if allText[i] not in (self.EMBEDDED_OBJECT_CHARACTER, self.ZERO_WIDTH_NO_BREAK_SPACE):
5181                        return obj, i
5182            elif obj.childCount and not self._treatObjectAsWhole(obj, offset):
5183                return self.findNextCaretInOrder(obj[0], -1)
5184            elif offset < 0 and not self.isTextBlockElement(obj):
5185                return obj, 0
5186
5187        # If we're here, start looking up the tree, up to the document.
5188        if self.isTopLevelDocument(obj):
5189            return None, -1
5190
5191        while obj and obj.parent:
5192            if self.isDetachedDocument(obj.parent):
5193                obj = self.iframeForDetachedDocument(obj.parent)
5194                continue
5195
5196            parent = obj.parent
5197            if self.isZombie(parent):
5198                msg = "WEB: Finding next caret in order. Parent is Zombie."
5199                debug.println(debug.LEVEL_INFO, msg, True)
5200                replicant = self.findReplicant(self.documentFrame(), parent)
5201                if replicant and not self.isZombie(replicant):
5202                    parent = replicant
5203                elif parent.parent:
5204                    obj = parent
5205                    continue
5206                else:
5207                    break
5208
5209            start, end, length = self._rangeInParentWithLength(obj)
5210            if start + 1 == end and 0 <= start < end <= length:
5211                return self.findNextCaretInOrder(parent, start)
5212
5213            index = obj.getIndexInParent() + 1
5214            try:
5215                parentChildCount = parent.childCount
5216            except:
5217                msg = "WEB: Exception getting childCount for %s" % parent
5218                debug.println(debug.LEVEL_INFO, msg, True)
5219            else:
5220                if 0 < index < parentChildCount:
5221                    return self.findNextCaretInOrder(parent[index], -1)
5222            obj = parent
5223
5224        return None, -1
5225
5226    def findPreviousCaretInOrder(self, obj=None, offset=-1):
5227        if not obj:
5228            obj, offset = self.getCaretContext()
5229
5230        if not obj or not self.inDocumentContent(obj):
5231            return None, -1
5232
5233        if self._canHaveCaretContext(obj):
5234            text = self.queryNonEmptyText(obj)
5235            if text:
5236                allText = text.getText(0, -1)
5237                if offset == -1 or offset > len(allText):
5238                    offset = len(allText)
5239                for i in range(offset - 1, -1, -1):
5240                    child = self.getChildAtOffset(obj, i)
5241                    if child and allText[i] != self.EMBEDDED_OBJECT_CHARACTER:
5242                        msg = "ERROR: Child %s found at offset with char '%s'" % \
5243                            (child, allText[i].replace("\n", "\\n"))
5244                        debug.println(debug.LEVEL_INFO, msg, True)
5245                    if self._canHaveCaretContext(child):
5246                        if self._treatObjectAsWhole(child, -1):
5247                            return child, 0
5248                        return self.findPreviousCaretInOrder(child, -1)
5249                    if allText[i] not in (self.EMBEDDED_OBJECT_CHARACTER, self.ZERO_WIDTH_NO_BREAK_SPACE):
5250                        return obj, i
5251            elif obj.childCount and not self._treatObjectAsWhole(obj, offset):
5252                return self.findPreviousCaretInOrder(obj[obj.childCount - 1], -1)
5253            elif offset < 0 and not self.isTextBlockElement(obj):
5254                return obj, 0
5255
5256        # If we're here, start looking up the tree, up to the document.
5257        if self.isTopLevelDocument(obj):
5258            return None, -1
5259
5260        while obj and obj.parent:
5261            if self.isDetachedDocument(obj.parent):
5262                obj = self.iframeForDetachedDocument(obj.parent)
5263                continue
5264
5265            parent = obj.parent
5266            if self.isZombie(parent):
5267                msg = "WEB: Finding previous caret in order. Parent is Zombie."
5268                debug.println(debug.LEVEL_INFO, msg, True)
5269                replicant = self.findReplicant(self.documentFrame(), parent)
5270                if replicant and not self.isZombie(replicant):
5271                    parent = replicant
5272                elif parent.parent:
5273                    obj = parent
5274                    continue
5275                else:
5276                    break
5277
5278            start, end, length = self._rangeInParentWithLength(obj)
5279            if start + 1 == end and 0 <= start < end <= length:
5280                return self.findPreviousCaretInOrder(parent, start)
5281
5282            index = obj.getIndexInParent() - 1
5283            try:
5284                parentChildCount = parent.childCount
5285            except:
5286                msg = "WEB: Exception getting childCount for %s" % parent
5287                debug.println(debug.LEVEL_INFO, msg, True)
5288            else:
5289                if 0 <= index < parentChildCount:
5290                    return self.findPreviousCaretInOrder(parent[index], -1)
5291            obj = parent
5292
5293        return None, -1
5294
5295    def lastQueuedLiveRegion(self):
5296        if self._lastQueuedLiveRegionEvent is None:
5297            return None
5298
5299        if self._lastQueuedLiveRegionEvent.type.startswith("object:text-changed:insert"):
5300            return self._lastQueuedLiveRegionEvent.source
5301
5302        if self._lastQueuedLiveRegionEvent.type.startswith("object:children-changed:add"):
5303            return self._lastQueuedLiveRegionEvent.any_data
5304
5305        return None
5306
5307    def handleAsLiveRegion(self, event):
5308        if not _settingsManager.getSetting('inferLiveRegions'):
5309            return False
5310
5311        if not self.isLiveRegion(event.source):
5312            return False
5313
5314        if not _settingsManager.getSetting('presentLiveRegionFromInactiveTab') \
5315           and self.getTopLevelDocumentForObject(event.source) != self.activeDocument():
5316            msg = "WEB: Live region source is not in active tab."
5317            debug.println(debug.LEVEL_INFO, msg, True)
5318            return False
5319
5320        if event.type.startswith("object:text-changed:insert"):
5321            alert = pyatspi.findAncestor(event.source, self.isAriaAlert)
5322            if alert and self.focusedObject(alert) == event.source:
5323                msg = "WEB: Focused source will be presented as part of alert"
5324                debug.println(debug.LEVEL_INFO, msg, True)
5325                return False
5326
5327            if self._lastQueuedLiveRegionEvent \
5328               and self._lastQueuedLiveRegionEvent.type == event.type \
5329               and self._lastQueuedLiveRegionEvent.any_data == event.any_data:
5330                msg = "WEB: Event is believed to be duplicate message"
5331                debug.println(debug.LEVEL_INFO, msg, True)
5332                return False
5333
5334        if isinstance(event.any_data, pyatspi.Accessible):
5335            try:
5336                role = event.any_data.getRole()
5337            except:
5338                msg = "WEB: Exception getting role for %s" % event.any_data
5339                debug.println(debug.LEVEL_INFO, msg, True)
5340                return False
5341
5342            if role in [pyatspi.ROLE_UNKNOWN, pyatspi.ROLE_REDUNDANT_OBJECT] \
5343               and self._getTag(event.any_data) in ["", None, "br"]:
5344                msg = "WEB: Child has unknown role and no tag %s" % event.any_data
5345                debug.println(debug.LEVEL_INFO, msg, True)
5346                return False
5347
5348            if self.lastQueuedLiveRegion() == event.any_data \
5349               and self._lastQueuedLiveRegionEvent.type != event.type:
5350                msg = "WEB: Event is believed to be redundant live region notification"
5351                debug.println(debug.LEVEL_INFO, msg, True)
5352                return False
5353
5354        self._lastQueuedLiveRegionEvent = event
5355        return True
5356
5357    def statusBar(self, obj):
5358        if self._statusBar and not self.isZombie(self._statusBar):
5359            msg = "WEB: Returning cached status bar: %s" % self._statusBar
5360            debug.println(debug.LEVEL_INFO, msg, True)
5361            return self._statusBar
5362
5363        self._statusBar = super().statusBar(obj)
5364        return self._statusBar
5365
5366    def getPageObjectCount(self, obj):
5367        result = {'landmarks': 0,
5368                  'headings': 0,
5369                  'forms': 0,
5370                  'tables': 0,
5371                  'visitedLinks': 0,
5372                  'unvisitedLinks': 0}
5373
5374        docframe = self.documentFrame(obj)
5375        msg = "WEB: Document frame for %s is %s" % (obj, docframe)
5376        debug.println(debug.LEVEL_INFO, msg, True)
5377
5378        col = docframe.queryCollection()
5379        stateset = pyatspi.StateSet()
5380        roles = [pyatspi.ROLE_HEADING,
5381                 pyatspi.ROLE_LINK,
5382                 pyatspi.ROLE_TABLE,
5383                 pyatspi.ROLE_FORM,
5384                 pyatspi.ROLE_LANDMARK]
5385
5386        if not self.supportsLandmarkRole():
5387            roles.append(pyatspi.ROLE_SECTION)
5388
5389        rule = col.createMatchRule(stateset.raw(), col.MATCH_NONE,
5390                                   "", col.MATCH_NONE,
5391                                   roles, col.MATCH_ANY,
5392                                   "", col.MATCH_NONE,
5393                                   False)
5394
5395        matches = col.getMatches(rule, col.SORT_ORDER_CANONICAL, 0, True)
5396        col.freeMatchRule(rule)
5397        for obj in matches:
5398            role = obj.getRole()
5399            if role == pyatspi.ROLE_HEADING:
5400                result['headings'] += 1
5401            elif role == pyatspi.ROLE_FORM:
5402                result['forms'] += 1
5403            elif role == pyatspi.ROLE_TABLE and not self.isLayoutOnly(obj):
5404                result['tables'] += 1
5405            elif role == pyatspi.ROLE_LINK:
5406                if self.isLink(obj):
5407                    if obj.getState().contains(pyatspi.STATE_VISITED):
5408                        result['visitedLinks'] += 1
5409                    else:
5410                        result['unvisitedLinks'] += 1
5411            elif self.isLandmark(obj):
5412                result['landmarks'] += 1
5413
5414        return result
5415
5416    def getPageSummary(self, obj, onlyIfFound=True):
5417        result = []
5418        counts = self.getPageObjectCount(obj)
5419        result.append(messages.landmarkCount(counts.get('landmarks', 0), onlyIfFound))
5420        result.append(messages.headingCount(counts.get('headings', 0), onlyIfFound))
5421        result.append(messages.formCount(counts.get('forms', 0), onlyIfFound))
5422        result.append(messages.tableCount(counts.get('tables', 0), onlyIfFound))
5423        result.append(messages.visitedLinkCount(counts.get('visitedLinks', 0), onlyIfFound))
5424        result.append(messages.unvisitedLinkCount(counts.get('unvisitedLinks', 0), onlyIfFound))
5425        result = list(filter(lambda x: x, result))
5426        if not result:
5427            return ""
5428
5429        return messages.PAGE_SUMMARY_PREFIX % ", ".join(result)
5430
5431    def preferDescriptionOverName(self, obj):
5432        if not self.inDocumentContent(obj):
5433            return super().preferDescriptionOverName(obj)
5434
5435        rv = self._preferDescriptionOverName.get(hash(obj))
5436        if rv is not None:
5437            return rv
5438
5439        try:
5440            role = obj.getRole()
5441            name = obj.name
5442            description = obj.description
5443        except:
5444            msg = "WEB: Exception getting name, description, and role for %s" % obj
5445            debug.println(debug.LEVEL_INFO, msg, True)
5446            rv = False
5447        else:
5448            if len(obj.name) == 1 and ord(obj.name) in range(0xe000, 0xf8ff):
5449                msg = "WEB: name of %s is in unicode private use area" % obj
5450                debug.println(debug.LEVEL_INFO, msg, True)
5451                rv = True
5452            else:
5453                roles = [pyatspi.ROLE_PUSH_BUTTON]
5454                rv = role in roles and len(name) == 1 and description
5455
5456        self._preferDescriptionOverName[hash(obj)] = rv
5457        return rv
5458
5459    def _getCtrlShiftSelectionsStrings(self):
5460        """Hacky and to-be-obsoleted method."""
5461        return [messages.LINE_SELECTED_DOWN,
5462                messages.LINE_UNSELECTED_DOWN,
5463                messages.LINE_SELECTED_UP,
5464                messages.LINE_UNSELECTED_UP]
5465
5466    def lastInputEventWasCopy(self):
5467        if super().lastInputEventWasCopy():
5468            return True
5469
5470        if not self.inDocumentContent():
5471            return False
5472
5473        if not self.topLevelObjectIsActiveAndCurrent():
5474            return False
5475
5476        if 'Action' in pyatspi.listInterfaces(orca_state.locusOfFocus):
5477            msg = "WEB: Treating %s as source of copy" % orca_state.locusOfFocus
5478            debug.println(debug.LEVEL_INFO, msg, True)
5479            return True
5480
5481        return False
5482